1#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
2use std::sync::{
3 atomic::{AtomicUsize, Ordering},
4 Mutex,
5};
6use std::sync::{Arc, LazyLock};
7
8#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
9use rand::distributions::Alphanumeric;
10#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
11use rand::{thread_rng, Rng};
12
13use crate::app;
14use crate::app::FirebaseApp;
15use crate::component::types::{
16 ComponentError, DynService, InstanceFactoryOptions, InstantiationMode,
17};
18use crate::component::{Component, ComponentType};
19#[cfg(all(
20 feature = "wasm-web",
21 target_arch = "wasm32",
22 feature = "experimental-indexed-db"
23))]
24use crate::installations::extract_app_config;
25#[cfg(all(
26 feature = "wasm-web",
27 target_arch = "wasm32",
28 feature = "experimental-indexed-db"
29))]
30use crate::installations::{get_installations_internal, InstallationEntryData};
31use crate::messaging::constants::MESSAGING_COMPONENT_NAME;
32#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
33use crate::messaging::error::invalid_argument;
34use crate::messaging::error::{
35 available_in_service_worker, available_in_window, internal_error, token_deletion_failed,
36 MessagingResult,
37};
38#[cfg(all(
39 feature = "wasm-web",
40 target_arch = "wasm32",
41 feature = "experimental-indexed-db"
42))]
43use crate::messaging::fcm_rest::{
44 FcmClient, FcmRegistrationRequest, FcmSubscription, FcmUpdateRequest,
45};
46#[cfg(all(
47 feature = "wasm-web",
48 target_arch = "wasm32",
49 feature = "experimental-indexed-db"
50))]
51use crate::messaging::token_store::{self, InstallationInfo, SubscriptionInfo, TokenRecord};
52#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
53use crate::messaging::token_store::{self, InstallationInfo, TokenRecord};
54#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
55use crate::messaging::types::MessagePayload;
56use crate::messaging::types::{MessageHandler, Unsubscribe};
57#[cfg(all(
58 feature = "wasm-web",
59 target_arch = "wasm32",
60 feature = "experimental-indexed-db"
61))]
62use std::time::{SystemTime, UNIX_EPOCH};
63#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
64use web_sys::NotificationPermission;
65
66#[cfg(all(
67 feature = "wasm-web",
68 target_arch = "wasm32",
69 feature = "experimental-indexed-db"
70))]
71use crate::messaging::constants::DEFAULT_VAPID_KEY;
72#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
73use crate::messaging::error::{permission_blocked, unsupported_browser};
74#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
75use wasm_bindgen::{JsCast, JsValue};
76#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
77use wasm_bindgen_futures::JsFuture;
78
79#[derive(Clone, Debug)]
80pub struct Messaging {
81 inner: Arc<MessagingInner>,
82}
83
84#[derive(Debug)]
85struct MessagingInner {
86 app: FirebaseApp,
87 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
88 on_message_handler: Mutex<Option<HandlerEntry>>,
89 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
90 on_background_message_handler: Mutex<Option<HandlerEntry>>,
91}
92
93#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
94#[derive(Clone)]
95struct HandlerEntry {
96 id: usize,
97 #[allow(dead_code)]
98 handler: MessageHandler,
99}
100
101#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
102impl std::fmt::Debug for HandlerEntry {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("HandlerEntry")
105 .field("id", &self.id)
106 .finish()
107 }
108}
109
110#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
111static NEXT_ON_MESSAGE_ID: AtomicUsize = AtomicUsize::new(1);
112
113#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
114static NEXT_ON_BACKGROUND_ID: AtomicUsize = AtomicUsize::new(1);
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum PermissionState {
119 Default,
121 Granted,
123 Denied,
125}
126
127impl Messaging {
128 fn new(app: FirebaseApp) -> Self {
129 let inner = MessagingInner {
130 app,
131 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
132 on_message_handler: Mutex::new(None),
133 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
134 on_background_message_handler: Mutex::new(None),
135 };
136 Self {
137 inner: Arc::new(inner),
138 }
139 }
140
141 pub fn app(&self) -> &FirebaseApp {
142 &self.inner.app
143 }
144
145 pub async fn request_permission(&self) -> MessagingResult<PermissionState> {
150 request_permission_impl().await
151 }
152
153 pub async fn get_token(&self, vapid_key: Option<&str>) -> MessagingResult<String> {
154 get_token_impl(self, vapid_key).await
155 }
156
157 pub async fn delete_token(&self) -> MessagingResult<bool> {
158 delete_token_impl(self).await
159 }
160
161 #[cfg_attr(not(test), allow(dead_code))]
162 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
163 #[allow(dead_code)]
164 pub(crate) fn dispatch_on_message(&self, payload: MessagePayload) {
165 let handler = {
166 self.inner
167 .on_message_handler
168 .lock()
169 .unwrap()
170 .as_ref()
171 .map(|entry| entry.handler.clone())
172 };
173 if let Some(handler) = handler {
174 handler(payload);
175 }
176 }
177
178 #[cfg_attr(not(test), allow(dead_code))]
179 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
180 #[allow(dead_code)]
181 pub(crate) fn dispatch_on_background_message(&self, payload: MessagePayload) {
182 let handler = {
183 self.inner
184 .on_background_message_handler
185 .lock()
186 .unwrap()
187 .as_ref()
188 .map(|entry| entry.handler.clone())
189 };
190 if let Some(handler) = handler {
191 handler(payload);
192 }
193 }
194}
195
196#[cfg(not(target_arch = "wasm32"))]
202fn generate_token() -> String {
203 thread_rng()
204 .sample_iter(&Alphanumeric)
205 .map(char::from)
206 .take(32)
207 .collect()
208}
209
210#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
211async fn request_permission_impl() -> MessagingResult<PermissionState> {
212 use crate::messaging::support::is_supported;
213
214 let window = web_sys::window()
215 .ok_or_else(|| available_in_window("request_permission must run in a Window context"))?;
216
217 let _navigator = window.navigator();
219
220 if !is_supported() {
221 return Err(unsupported_browser(
222 "This browser does not expose the APIs required for Firebase Messaging.",
223 ));
224 }
225
226 let current = web_sys::Notification::permission();
227 match permission_state_from_enum(current) {
228 PermissionState::Granted => return Ok(PermissionState::Granted),
229 PermissionState::Denied => {
230 return Err(permission_blocked(
231 "Notification permission was previously blocked by the user.",
232 ))
233 }
234 PermissionState::Default => {}
235 }
236
237 let promise = web_sys::Notification::request_permission()
238 .map_err(|err| internal_error(format_js_error("requestPermission", err)))?;
239 let result = JsFuture::from(promise)
240 .await
241 .map_err(|err| internal_error(format_js_error("requestPermission", err)))?;
242
243 let status = result.as_string();
244 let permission_state = status
245 .as_deref()
246 .map(permission_state_from_str)
247 .unwrap_or_else(|| permission_state_from_enum(web_sys::Notification::permission()));
248
249 match permission_state {
250 PermissionState::Granted => Ok(PermissionState::Granted),
251 PermissionState::Denied | PermissionState::Default => Err(permission_blocked(
252 "Notification permission not granted by the user.",
253 )),
254 }
255}
256
257#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
258fn permission_state_from_enum(value: NotificationPermission) -> PermissionState {
259 match value {
260 NotificationPermission::Granted => PermissionState::Granted,
261 NotificationPermission::Denied => PermissionState::Denied,
262 _ => PermissionState::Default,
263 }
264}
265
266#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
267fn permission_state_from_str(value: &str) -> PermissionState {
268 match value {
269 "granted" => PermissionState::Granted,
270 "denied" => PermissionState::Denied,
271 _ => PermissionState::Default,
272 }
273}
274
275#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
276fn format_js_error(operation: &str, err: JsValue) -> String {
277 if let Some(message) = err.as_string() {
278 format!("{operation} failed: {message}")
279 } else if let Some(exception) = err.dyn_ref::<web_sys::DomException>() {
280 format!(
281 "{operation} failed: {}: {}",
282 exception.name(),
283 exception.message()
284 )
285 } else {
286 format!("{operation} failed: {:?}", err)
287 }
288}
289
290#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
291async fn request_permission_impl() -> MessagingResult<PermissionState> {
292 Ok(PermissionState::Granted)
293}
294
295static MESSAGING_COMPONENT: LazyLock<()> = LazyLock::new(|| {
296 let component = Component::new(
297 MESSAGING_COMPONENT_NAME,
298 Arc::new(messaging_factory),
299 ComponentType::Public,
300 )
301 .with_instantiation_mode(InstantiationMode::Lazy);
302 let _ = app::register_component(component);
303});
304
305fn messaging_factory(
306 container: &crate::component::ComponentContainer,
307 _options: InstanceFactoryOptions,
308) -> Result<DynService, ComponentError> {
309 let app = container.root_service::<FirebaseApp>().ok_or_else(|| {
310 ComponentError::InitializationFailed {
311 name: MESSAGING_COMPONENT_NAME.to_string(),
312 reason: "Firebase app not attached to component container".to_string(),
313 }
314 })?;
315 let messaging = Messaging::new((*app).clone());
316 Ok(Arc::new(messaging) as DynService)
317}
318
319fn ensure_registered() {
320 LazyLock::force(&MESSAGING_COMPONENT);
321}
322
323pub fn register_messaging_component() {
324 ensure_registered();
325}
326
327pub async fn get_messaging(app: Option<FirebaseApp>) -> MessagingResult<Arc<Messaging>> {
328 ensure_registered();
329 let app = match app {
330 Some(app) => app,
331 None => crate::app::get_app(None)
332 .await
333 .map_err(|err| internal_error(err.to_string()))?,
334 };
335
336 let provider = app::get_provider(&app, MESSAGING_COMPONENT_NAME);
337 if let Some(messaging) = provider.get_immediate::<Messaging>() {
338 Ok(messaging)
339 } else {
340 provider
341 .initialize::<Messaging>(serde_json::Value::Null, None)
342 .map_err(|err| internal_error(err.to_string()))
343 }
344}
345
346async fn get_token_impl(messaging: &Messaging, vapid_key: Option<&str>) -> MessagingResult<String> {
347 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
348 {
349 get_token_wasm(messaging, vapid_key).await
350 }
351
352 #[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
353 {
354 get_token_native(messaging, vapid_key).await
355 }
356}
357
358#[allow(dead_code)]
359fn app_store_key(messaging: &Messaging) -> String {
360 messaging.inner.app.name().to_string()
361}
362
363#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
364pub fn on_message(messaging: &Messaging, handler: MessageHandler) -> MessagingResult<Unsubscribe> {
365 if web_sys::window().is_none() {
366 return Err(available_in_window(
367 "on_message must be called in a Window context",
368 ));
369 }
370
371 let id = NEXT_ON_MESSAGE_ID.fetch_add(1, Ordering::SeqCst);
372 let messaging_clone = messaging.clone();
373 {
374 let mut guard = messaging_clone.inner.on_message_handler.lock().unwrap();
375 *guard = Some(HandlerEntry { id, handler });
376 }
377
378 Ok(Box::new(move || {
379 let mut guard = messaging_clone.inner.on_message_handler.lock().unwrap();
380 if guard.as_ref().map(|entry| entry.id) == Some(id) {
381 *guard = None;
382 }
383 }))
384}
385
386#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
387pub fn on_message(
388 _messaging: &Messaging,
389 _handler: MessageHandler,
390) -> MessagingResult<Unsubscribe> {
391 Err(available_in_window(
392 "on_message must be called in a Window context (wasm target only)",
393 ))
394}
395
396#[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
397pub fn on_background_message(
398 messaging: &Messaging,
399 handler: MessageHandler,
400) -> MessagingResult<Unsubscribe> {
401 if web_sys::window().is_some() {
402 return Err(available_in_service_worker(
403 "on_background_message must be called in a Service Worker context",
404 ));
405 }
406
407 let id = NEXT_ON_BACKGROUND_ID.fetch_add(1, Ordering::SeqCst);
408 let messaging_clone = messaging.clone();
409 {
410 let mut guard = messaging_clone
411 .inner
412 .on_background_message_handler
413 .lock()
414 .unwrap();
415 *guard = Some(HandlerEntry { id, handler });
416 }
417
418 Ok(Box::new(move || {
419 let mut guard = messaging_clone
420 .inner
421 .on_background_message_handler
422 .lock()
423 .unwrap();
424 if guard.as_ref().map(|entry| entry.id) == Some(id) {
425 *guard = None;
426 }
427 }))
428}
429
430#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
431pub fn on_background_message(
432 _messaging: &Messaging,
433 _handler: MessageHandler,
434) -> MessagingResult<Unsubscribe> {
435 Err(available_in_service_worker(
436 "on_background_message must be called in a Service Worker context (wasm target only)",
437 ))
438}
439
440#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
441async fn get_token_native(
442 messaging: &Messaging,
443 vapid_key: Option<&str>,
444) -> MessagingResult<String> {
445 if let Some(key) = vapid_key {
446 if key.trim().is_empty() {
447 return Err(invalid_argument("VAPID key must not be empty"));
448 }
449 }
450
451 let store_key = app_store_key(messaging);
452 if let Some(record) = token_store::read_token(&store_key)? {
453 if !record.is_expired(current_timestamp_ms(), TOKEN_EXPIRATION_MS) {
454 return Ok(record.token);
455 }
456 }
457
458 let token = generate_token();
459 let record = TokenRecord {
460 token: token.clone(),
461 create_time_ms: current_timestamp_ms(),
462 subscription: None,
463 installation: dummy_installation_info(),
464 };
465 token_store::write_token(&store_key, &record)?;
466 Ok(token)
467}
468
469#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
470async fn delete_token_impl(messaging: &Messaging) -> MessagingResult<bool> {
471 let store_key = app_store_key(messaging);
472 if token_store::remove_token(&store_key)? {
473 Ok(true)
474 } else {
475 Err(token_deletion_failed("No token stored for this app"))
476 }
477}
478
479#[cfg(all(
480 feature = "wasm-web",
481 target_arch = "wasm32",
482 feature = "experimental-indexed-db"
483))]
484async fn get_token_wasm(messaging: &Messaging, vapid_key: Option<&str>) -> MessagingResult<String> {
485 use crate::messaging::subscription::PushSubscriptionManager;
486 use crate::messaging::support::is_supported;
487 use crate::messaging::sw_manager::ServiceWorkerManager;
488
489 if !is_supported() {
490 return Err(unsupported_browser(
491 "This browser does not expose the APIs required for Firebase Messaging.",
492 ));
493 }
494
495 let vapid_key = vapid_key
496 .filter(|key| !key.trim().is_empty())
497 .unwrap_or(DEFAULT_VAPID_KEY);
498 let app_config =
499 extract_app_config(messaging.app()).map_err(|err| internal_error(err.to_string()))?;
500 let fcm_client = FcmClient::new()?;
501
502 let mut sw_manager = ServiceWorkerManager::new();
503 let registration = sw_manager.register_default().await?;
504 let scope = registration.as_web_sys().scope();
505
506 let mut push_manager = PushSubscriptionManager::new();
507 let subscription = push_manager.subscribe(®istration, vapid_key).await?;
508 let details = subscription.details()?;
509
510 let subscription_info = SubscriptionInfo {
511 vapid_key: vapid_key.to_string(),
512 scope,
513 endpoint: details.endpoint.clone(),
514 auth: details.auth.clone(),
515 p256dh: details.p256dh.clone(),
516 };
517
518 let store_key = app_store_key(messaging);
519 let now_ms = current_timestamp_ms();
520 let subscription_payload = FcmSubscription {
521 endpoint: &subscription_info.endpoint,
522 auth: &subscription_info.auth,
523 p256dh: &subscription_info.p256dh,
524 application_pub_key: if subscription_info.vapid_key == DEFAULT_VAPID_KEY {
525 None
526 } else {
527 Some(subscription_info.vapid_key.as_str())
528 },
529 };
530
531 if let Some(record) = token_store::read_token(&store_key).await? {
532 if let Some(existing) = &record.subscription {
533 if existing == &subscription_info {
534 let installation_needs_refresh = record.installation.auth_token_expired(now_ms);
535 if !record.is_expired(now_ms, TOKEN_EXPIRATION_MS) && !installation_needs_refresh {
536 return Ok(record.token);
537 }
538
539 let installation_info =
540 fetch_installation_info(messaging, installation_needs_refresh).await?;
541 let update_request = FcmUpdateRequest {
542 registration_token: &record.token,
543 registration: FcmRegistrationRequest {
544 project_id: &app_config.project_id,
545 api_key: &app_config.api_key,
546 installation_auth_token: &installation_info.auth_token,
547 subscription: subscription_payload.clone(),
548 },
549 };
550
551 let token = fcm_client.update_token(&update_request).await?;
552 let record = TokenRecord {
553 token: token.clone(),
554 create_time_ms: now_ms,
555 subscription: Some(subscription_info),
556 installation: installation_info,
557 };
558 token_store::write_token(&store_key, &record).await?;
559 return Ok(token);
560 }
561 }
562
563 let installation_info = fetch_installation_info(messaging, true).await?;
564 let _ = fcm_client
565 .delete_token(
566 &app_config.project_id,
567 &app_config.api_key,
568 &installation_info.auth_token,
569 &record.token,
570 )
571 .await;
572 let _ = token_store::remove_token(&store_key).await?;
573 }
574
575 let installation_info = fetch_installation_info(messaging, true).await?;
576 let registration_request = FcmRegistrationRequest {
577 project_id: &app_config.project_id,
578 api_key: &app_config.api_key,
579 installation_auth_token: &installation_info.auth_token,
580 subscription: subscription_payload,
581 };
582
583 let token = fcm_client.register_token(®istration_request).await?;
584 let record = TokenRecord {
585 token: token.clone(),
586 create_time_ms: now_ms,
587 subscription: Some(subscription_info),
588 installation: installation_info,
589 };
590 token_store::write_token(&store_key, &record).await?;
591 Ok(token)
592}
593
594#[cfg(all(
595 feature = "wasm-web",
596 target_arch = "wasm32",
597 not(feature = "experimental-indexed-db")
598))]
599async fn get_token_wasm(_: &Messaging, _: Option<&str>) -> MessagingResult<String> {
600 Err(unsupported_browser(
601 "Firebase Messaging token persistence requires the `experimental-indexed-db` feature on wasm targets.",
602 ))
603}
604
605#[cfg(all(
606 feature = "wasm-web",
607 target_arch = "wasm32",
608 feature = "experimental-indexed-db"
609))]
610async fn delete_token_impl(messaging: &Messaging) -> MessagingResult<bool> {
611 use crate::messaging::subscription::PushSubscriptionManager;
612 use crate::messaging::sw_manager::ServiceWorkerManager;
613
614 let store_key = app_store_key(messaging);
615 let record = match token_store::read_token(&store_key).await? {
616 Some(record) => record,
617 None => return Err(token_deletion_failed("No token stored for this app")),
618 };
619
620 let app_config =
621 extract_app_config(messaging.app()).map_err(|err| internal_error(err.to_string()))?;
622 let installation_info = fetch_installation_info(messaging, true).await?;
623 let fcm_client = FcmClient::new()?;
624
625 fcm_client
626 .delete_token(
627 &app_config.project_id,
628 &app_config.api_key,
629 &installation_info.auth_token,
630 &record.token,
631 )
632 .await?;
633
634 let removed = token_store::remove_token(&store_key).await?;
635
636 let mut sw_manager = ServiceWorkerManager::new();
637 let registration = sw_manager.register_default().await?;
638 let sw_registration = registration.as_web_sys();
639 let push_manager = sw_registration
640 .push_manager()
641 .map_err(|err| internal_error(format_js_error("pushManager", err)))?;
642 let subscription_value = JsFuture::from(
643 push_manager
644 .get_subscription()
645 .map_err(|err| internal_error(format_js_error("getSubscription", err)))?,
646 )
647 .await
648 .map_err(|err| internal_error(format_js_error("getSubscription", err)))?;
649
650 if !subscription_value.is_null() && !subscription_value.is_undefined() {
651 let subscription: web_sys::PushSubscription = subscription_value
652 .dyn_into()
653 .map_err(|_| internal_error("PushManager.getSubscription returned unexpected value"))?;
654 let promise = subscription
655 .unsubscribe()
656 .map_err(|err| internal_error(format_js_error("PushSubscription.unsubscribe", err)))?;
657 let _ = JsFuture::from(promise)
658 .await
659 .map_err(|err| internal_error(format_js_error("PushSubscription.unsubscribe", err)))?;
660 }
661
662 let mut push_manager = PushSubscriptionManager::new();
663 push_manager.clear_cache();
664
665 Ok(removed)
666}
667
668#[cfg(all(
669 feature = "wasm-web",
670 target_arch = "wasm32",
671 not(feature = "experimental-indexed-db")
672))]
673async fn delete_token_impl(_: &Messaging) -> MessagingResult<bool> {
674 Err(token_deletion_failed(
675 "Token deletion is unavailable without the `experimental-indexed-db` feature on wasm targets.",
676 ))
677}
678
679#[cfg(all(
680 feature = "wasm-web",
681 target_arch = "wasm32",
682 feature = "experimental-indexed-db"
683))]
684async fn fetch_installation_info(
685 messaging: &Messaging,
686 force_refresh: bool,
687) -> MessagingResult<InstallationInfo> {
688 let internal = get_installations_internal(Some(messaging.inner.app.clone()))
689 .map_err(|err| internal_error(format!("Failed to initialise installations: {err}")))?;
690
691 let InstallationEntryData {
692 fid,
693 refresh_token,
694 mut auth_token,
695 } = internal
696 .get_installation_entry()
697 .await
698 .map_err(|err| internal_error(format!("Failed to load installation entry: {err}")))?;
699
700 if force_refresh || auth_token.is_expired() {
701 auth_token = internal.get_token(true).await.map_err(|err| {
702 internal_error(format!("Failed to refresh installation token: {err}"))
703 })?;
704 }
705
706 let expires_at_ms = system_time_to_millis(auth_token.expires_at)?;
707
708 Ok(InstallationInfo {
709 fid,
710 refresh_token,
711 auth_token: auth_token.token,
712 auth_token_expiration_ms: expires_at_ms,
713 })
714}
715
716#[cfg(all(
717 feature = "wasm-web",
718 target_arch = "wasm32",
719 feature = "experimental-indexed-db"
720))]
721fn system_time_to_millis(time: SystemTime) -> MessagingResult<u64> {
722 time.duration_since(UNIX_EPOCH)
723 .map(|duration| duration.as_millis() as u64)
724 .map_err(|_| internal_error("Installation token expiration precedes UNIX epoch"))
725}
726
727#[allow(dead_code)]
728fn current_timestamp_ms() -> u64 {
729 #[cfg(all(feature = "wasm-web", target_arch = "wasm32"))]
730 {
731 js_sys::Date::now() as u64
732 }
733
734 #[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
735 {
736 use std::time::{SystemTime, UNIX_EPOCH};
737
738 SystemTime::now()
739 .duration_since(UNIX_EPOCH)
740 .unwrap_or_default()
741 .as_millis() as u64
742 }
743}
744
745#[allow(dead_code)]
746const TOKEN_EXPIRATION_MS: u64 = 7 * 24 * 60 * 60 * 1000;
747
748#[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
749fn dummy_installation_info() -> InstallationInfo {
750 InstallationInfo {
751 fid: "placeholder".to_string(),
752 refresh_token: String::new(),
753 auth_token: String::new(),
754 auth_token_expiration_ms: u64::MAX,
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761 use crate::app::initialize_app;
762 use crate::app::{FirebaseAppSettings, FirebaseOptions};
763 #[allow(unused_imports)]
764 use std::task::Waker;
765 use std::task::{RawWaker, RawWakerVTable};
766
767 fn unique_settings() -> FirebaseAppSettings {
768 use std::sync::atomic::{AtomicUsize, Ordering};
769 static COUNTER: AtomicUsize = AtomicUsize::new(0);
770 FirebaseAppSettings {
771 name: Some(format!(
772 "messaging-{}",
773 COUNTER.fetch_add(1, Ordering::SeqCst)
774 )),
775 ..Default::default()
776 }
777 }
778
779 #[tokio::test(flavor = "current_thread")]
780 async fn token_is_stable_until_deleted() {
781 let options = FirebaseOptions {
782 project_id: Some("project".into()),
783 ..Default::default()
784 };
785 let app = initialize_app(options, Some(unique_settings()))
786 .await
787 .expect("init app");
788 let messaging = get_messaging(Some(app)).await.unwrap();
789 let permission = messaging.request_permission().await.unwrap();
790 assert_eq!(permission, PermissionState::Granted);
791 let token1 = messaging.get_token(None).await.unwrap();
792 let token2 = messaging.get_token(None).await.unwrap();
793 assert_eq!(token1, token2);
794 messaging.delete_token().await.unwrap();
795 let token3 = messaging.get_token(None).await.unwrap();
796 assert_ne!(token1, token3);
797 }
798
799 #[tokio::test(flavor = "current_thread")]
800 async fn get_token_with_empty_vapid_key_returns_error() {
801 let options = FirebaseOptions {
802 project_id: Some("project".into()),
803 ..Default::default()
804 };
805 let app = initialize_app(options, Some(unique_settings()))
806 .await
807 .expect("init app");
808 let messaging = get_messaging(Some(app)).await.unwrap();
809 let err = messaging.get_token(Some(" ")).await.unwrap_err();
810 assert_eq!(err.code_str(), "messaging/invalid-argument");
811 }
812
813 #[tokio::test(flavor = "current_thread")]
814 async fn delete_token_without_existing_token_returns_error() {
815 let options = FirebaseOptions {
816 project_id: Some("project".into()),
817 ..Default::default()
818 };
819 let app = initialize_app(options, Some(unique_settings()))
820 .await
821 .expect("init app");
822 let messaging = get_messaging(Some(app)).await.unwrap();
823 let err = messaging.delete_token().await.unwrap_err();
824 assert_eq!(err.code_str(), "messaging/token-deletion-failed");
825 }
826
827 #[tokio::test(flavor = "current_thread")]
828 async fn token_persists_across_messaging_instances() {
829 let options = FirebaseOptions {
830 project_id: Some("project".into()),
831 ..Default::default()
832 };
833 let app = initialize_app(options, Some(unique_settings()))
834 .await
835 .expect("init app");
836 let messaging = get_messaging(Some(app.clone())).await.unwrap();
837 let token1 = messaging.get_token(None).await.unwrap();
838
839 let messaging_again = get_messaging(Some(app)).await.unwrap();
841 let token2 = messaging_again.get_token(None).await.unwrap();
842 assert_eq!(token1, token2);
843
844 messaging_again.delete_token().await.unwrap();
845 }
846
847 #[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
848 #[tokio::test(flavor = "current_thread")]
849 async fn on_message_returns_window_error_on_non_wasm() {
850 let options = FirebaseOptions {
851 project_id: Some("project".into()),
852 ..Default::default()
853 };
854 let app = initialize_app(options, Some(unique_settings()))
855 .await
856 .expect("init app");
857 let messaging = get_messaging(Some(app)).await.unwrap();
858
859 let handler: MessageHandler = Arc::new(|_| {});
860 let err = match super::on_message(&messaging, handler) {
861 Ok(unsub) => {
862 unsub();
863 panic!("expected on_message to fail on non-wasm targets");
864 }
865 Err(err) => err,
866 };
867 assert_eq!(err.code_str(), "messaging/available-in-window");
868 }
869
870 #[cfg(not(all(feature = "wasm-web", target_arch = "wasm32")))]
871 #[tokio::test(flavor = "current_thread")]
872 async fn on_background_message_returns_sw_error_on_non_wasm() {
873 let options = FirebaseOptions {
874 project_id: Some("project".into()),
875 ..Default::default()
876 };
877 let app = initialize_app(options, Some(unique_settings()))
878 .await
879 .expect("init app");
880 let messaging = get_messaging(Some(app)).await.unwrap();
881
882 let handler: MessageHandler = Arc::new(|_| {});
883 let err = match super::on_background_message(&messaging, handler) {
884 Ok(unsub) => {
885 unsub();
886 panic!("expected on_background_message to fail on non-wasm targets");
887 }
888 Err(err) => err,
889 };
890 assert_eq!(err.code_str(), "messaging/available-in-sw");
891 }
892
893 #[allow(dead_code)]
894 fn noop_raw_waker() -> RawWaker {
895 RawWaker::new(std::ptr::null(), &NOOP_RAW_WAKER_VTABLE)
896 }
897
898 #[allow(dead_code)]
899 unsafe fn noop_raw_waker_clone(_: *const ()) -> RawWaker {
900 noop_raw_waker()
901 }
902
903 #[allow(dead_code)]
904 unsafe fn noop_raw_waker_wake(_: *const ()) {}
905
906 #[allow(dead_code)]
907 unsafe fn noop_raw_waker_wake_by_ref(_: *const ()) {}
908
909 #[allow(dead_code)]
910 unsafe fn noop_raw_waker_drop(_: *const ()) {}
911
912 #[allow(dead_code)]
913 static NOOP_RAW_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
914 noop_raw_waker_clone,
915 noop_raw_waker_wake,
916 noop_raw_waker_wake_by_ref,
917 noop_raw_waker_drop,
918 );
919}