firebase_rs_sdk/messaging/
subscription.rs1#[cfg(any(
7 not(all(feature = "wasm-web", target_arch = "wasm32")),
8 all(
9 feature = "wasm-web",
10 target_arch = "wasm32",
11 not(feature = "experimental-indexed-db")
12 )
13))]
14use crate::messaging::error::{unsupported_browser, MessagingResult};
15
16#[cfg(all(
17 feature = "wasm-web",
18 target_arch = "wasm32",
19 feature = "experimental-indexed-db"
20))]
21mod wasm {
22 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
23 use base64::Engine;
24 use wasm_bindgen::JsCast;
25 use wasm_bindgen::JsValue;
26 use wasm_bindgen_futures::JsFuture;
27
28 use crate::messaging::error::{
29 internal_error, invalid_argument, token_subscribe_failed, token_unsubscribe_failed,
30 unsupported_browser, MessagingResult,
31 };
32 use crate::messaging::sw_manager::ServiceWorkerRegistrationHandle;
33
34 #[derive(Clone, Debug, PartialEq, Eq)]
36 pub struct PushSubscriptionDetails {
37 pub endpoint: String,
38 pub auth: String,
39 pub p256dh: String,
40 }
41
42 #[derive(Clone)]
44 pub struct PushSubscriptionHandle {
45 inner: web_sys::PushSubscription,
46 }
47
48 impl PushSubscriptionHandle {
49 fn new(inner: web_sys::PushSubscription) -> Self {
50 Self { inner }
51 }
52
53 pub fn as_web_sys(&self) -> &web_sys::PushSubscription {
54 &self.inner
55 }
56
57 pub fn details(&self) -> MessagingResult<PushSubscriptionDetails> {
58 let endpoint = self.inner.endpoint();
59 if endpoint.trim().is_empty() {
60 return Err(token_subscribe_failed("Push subscription missing endpoint"));
61 }
62 let auth = extract_key(&self.inner, "auth")?;
63 let p256dh = extract_key(&self.inner, "p256dh")?;
64
65 Ok(PushSubscriptionDetails {
66 endpoint,
67 auth,
68 p256dh,
69 })
70 }
71
72 pub async fn unsubscribe(self) -> MessagingResult<bool> {
73 let promise = self
74 .inner
75 .unsubscribe()
76 .map_err(|err| token_unsubscribe_failed(format_js_error("unsubscribe", err)))?;
77
78 let result = JsFuture::from(promise)
79 .await
80 .map_err(|err| token_unsubscribe_failed(format_js_error("unsubscribe", err)))?;
81
82 Ok(result.as_bool().unwrap_or(true))
83 }
84 }
85
86 #[derive(Default)]
87 pub struct PushSubscriptionManager {
88 cached: Option<PushSubscriptionHandle>,
89 }
90
91 impl PushSubscriptionManager {
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn cached_subscription(&self) -> Option<PushSubscriptionHandle> {
97 self.cached.clone()
98 }
99
100 pub async fn subscribe(
101 &mut self,
102 registration: &ServiceWorkerRegistrationHandle,
103 vapid_key: &str,
104 ) -> MessagingResult<PushSubscriptionHandle> {
105 if let Some(handle) = &self.cached {
106 return Ok(handle.clone());
107 }
108
109 let sw_registration = registration.as_web_sys();
110 let push_manager = sw_registration
111 .push_manager()
112 .map_err(|err| unsupported_browser(format_js_error("pushManager", err)))?;
113
114 let existing = JsFuture::from(
115 push_manager
116 .get_subscription()
117 .map_err(|err| internal_error(format_js_error("getSubscription", err)))?,
118 )
119 .await
120 .map_err(|err| internal_error(format_js_error("getSubscription", err)))?;
121
122 let subscription = if existing.is_undefined() || existing.is_null() {
123 let options = web_sys::PushSubscriptionOptionsInit::new();
124 options.set_user_visible_only(true);
125 let application_server_key = vapid_key_to_uint8_array(vapid_key)?;
126 let key_value: JsValue = application_server_key.into();
127 options.set_application_server_key(&key_value);
128
129 let promise = push_manager
130 .subscribe_with_options(&options)
131 .map_err(|err| token_subscribe_failed(format_js_error("subscribe", err)))?;
132
133 let value = JsFuture::from(promise)
134 .await
135 .map_err(|err| token_subscribe_failed(format_js_error("subscribe", err)))?;
136
137 value.dyn_into().map_err(|_| {
138 token_subscribe_failed("PushManager.subscribe returned unexpected value")
139 })?
140 } else {
141 existing.dyn_into().map_err(|_| {
142 token_subscribe_failed("getSubscription returned unexpected value")
143 })?
144 };
145
146 let handle = PushSubscriptionHandle::new(subscription);
147 self.cached = Some(handle.clone());
148 Ok(handle)
149 }
150
151 pub fn clear_cache(&mut self) {
152 self.cached = None;
153 }
154 }
155
156 fn vapid_key_to_uint8_array(vapid_key: &str) -> MessagingResult<js_sys::Uint8Array> {
157 let trimmed = vapid_key.trim();
158 if trimmed.is_empty() {
159 return Err(invalid_argument("VAPID key must not be empty"));
160 }
161 let bytes = URL_SAFE_NO_PAD
162 .decode(trimmed)
163 .map_err(|err| invalid_argument(format!("Invalid VAPID key: {err}")))?;
164 Ok(js_sys::Uint8Array::from(bytes.as_slice()))
165 }
166
167 fn extract_key(
168 subscription: &web_sys::PushSubscription,
169 name: &str,
170 ) -> MessagingResult<String> {
171 let key_name = match name {
172 "p256dh" => web_sys::PushEncryptionKeyName::P256dh,
173 "auth" => web_sys::PushEncryptionKeyName::Auth,
174 other => {
175 return Err(token_subscribe_failed(format!(
176 "Unsupported push encryption key name: {other}"
177 )))
178 }
179 };
180 let buffer = subscription.get_key(key_name).map_err(|err| {
181 token_subscribe_failed(format_js_error("PushSubscription.getKey", err))
182 })?;
183 if let Some(buffer) = buffer {
184 let view = js_sys::Uint8Array::new(&buffer);
185 Ok(base64::engine::general_purpose::STANDARD.encode(view.to_vec()))
186 } else {
187 Err(token_subscribe_failed(format!(
188 "Push subscription missing {name} key"
189 )))
190 }
191 }
192
193 fn format_js_error(operation: &str, err: JsValue) -> String {
194 let detail = err.as_string().unwrap_or_else(|| format!("{:?}", err));
195 format!("{operation} failed: {detail}")
196 }
197
198 pub use PushSubscriptionDetails as Details;
199 pub use PushSubscriptionHandle as Handle;
200 pub use PushSubscriptionManager as Manager;
201}
202
203#[cfg(all(
204 feature = "wasm-web",
205 target_arch = "wasm32",
206 feature = "experimental-indexed-db"
207))]
208pub use wasm::{
209 Details as PushSubscriptionDetails, Handle as PushSubscriptionHandle,
210 Manager as PushSubscriptionManager,
211};
212
213#[cfg(any(
214 not(all(feature = "wasm-web", target_arch = "wasm32")),
215 all(
216 feature = "wasm-web",
217 target_arch = "wasm32",
218 not(feature = "experimental-indexed-db")
219 )
220))]
221#[derive(Default)]
222pub struct PushSubscriptionManager;
223
224#[cfg(any(
225 not(all(feature = "wasm-web", target_arch = "wasm32")),
226 all(
227 feature = "wasm-web",
228 target_arch = "wasm32",
229 not(feature = "experimental-indexed-db")
230 )
231))]
232impl PushSubscriptionManager {
233 pub fn new() -> Self {
234 Self
235 }
236
237 pub fn cached_subscription(&self) -> Option<PushSubscriptionHandle> {
238 None
239 }
240
241 pub async fn subscribe(
242 &mut self,
243 _registration: &crate::messaging::ServiceWorkerRegistrationHandle,
244 _vapid_key: &str,
245 ) -> MessagingResult<PushSubscriptionHandle> {
246 Err(unsupported_browser(
247 "Push subscriptions are only available when the `wasm-web` feature is enabled.",
248 ))
249 }
250
251 pub fn clear_cache(&mut self) {}
252}
253
254#[cfg(any(
255 not(all(feature = "wasm-web", target_arch = "wasm32")),
256 all(
257 feature = "wasm-web",
258 target_arch = "wasm32",
259 not(feature = "experimental-indexed-db")
260 )
261))]
262#[derive(Clone, Debug)]
263pub struct PushSubscriptionHandle;
264
265#[cfg(any(
266 not(all(feature = "wasm-web", target_arch = "wasm32")),
267 all(
268 feature = "wasm-web",
269 target_arch = "wasm32",
270 not(feature = "experimental-indexed-db")
271 )
272))]
273#[allow(dead_code)]
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub struct PushSubscriptionDetails;
276
277#[cfg(all(
278 test,
279 any(
280 not(all(feature = "wasm-web", target_arch = "wasm32")),
281 all(
282 feature = "wasm-web",
283 target_arch = "wasm32",
284 not(feature = "experimental-indexed-db")
285 )
286 )
287))]
288mod tests {
289 use super::*;
290
291 #[tokio::test(flavor = "current_thread")]
292 async fn native_subscribe_reports_unsupported() {
293 let mut manager = PushSubscriptionManager::new();
294 manager.clear_cache();
295 assert!(manager.cached_subscription().is_none());
296 let registration = crate::messaging::ServiceWorkerRegistrationHandle;
297 let err = manager.subscribe(®istration, "test").await.unwrap_err();
298 assert_eq!(err.code_str(), "messaging/unsupported-browser");
299 }
300}