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