1import Constants from 'expo-constants'; 2import { CodedError, Platform, SyntheticPlatformEmitter } from 'expo-modules-core'; 3 4import { DevicePushToken } from './Tokens.types'; 5 6export default async function getDevicePushTokenAsync(): Promise<DevicePushToken> { 7 const data = await _subscribeDeviceToPushNotificationsAsync(); 8 SyntheticPlatformEmitter.emit('onDevicePushToken', { devicePushToken: data }); 9 return { type: Platform.OS, data }; 10} 11 12function guardPermission() { 13 if (!('Notification' in window)) { 14 throw new CodedError( 15 'ERR_UNAVAILABLE', 16 'The Web Notifications API is not available on this device.' 17 ); 18 } 19 if (!navigator.serviceWorker) { 20 throw new CodedError( 21 'ERR_UNAVAILABLE', 22 'Notifications cannot be used because the service worker API is not supported on this device. This might also happen because your web page does not support HTTPS.' 23 ); 24 } 25 if (Notification.permission !== 'granted') { 26 throw new CodedError( 27 'ERR_NOTIFICATIONS_PERMISSION_DENIED', 28 `Cannot use web notifications without permissions granted. Request permissions with "expo-permissions".` 29 ); 30 } 31} 32 33async function _subscribeDeviceToPushNotificationsAsync(): Promise<DevicePushToken['data']> { 34 const vapidPublicKey: string | null = 35 // @ts-expect-error: TODO: not on the schema 36 Constants.manifest?.notification?.vapidPublicKey ?? 37 // @ts-expect-error: TODO: not on the schema 38 Constants.manifest2?.extra?.expoClient?.notification?.vapidPublicKey; 39 if (!vapidPublicKey) { 40 throw new CodedError( 41 'ERR_NOTIFICATIONS_PUSH_WEB_MISSING_CONFIG', 42 'You must provide `notification.vapidPublicKey` in `app.json` to use push notifications on web. Learn more: https://docs.expo.dev/versions/latest/guides/using-vapid/.' 43 ); 44 } 45 46 const serviceWorkerPath = 47 // @ts-expect-error: TODO: not on the schema 48 Constants.manifest?.notification.serviceWorkerPath ?? 49 // @ts-expect-error: TODO: not on the schema 50 Constants.manifest2?.extra?.expoClient?.notification?.serviceWorkerPath; 51 if (!serviceWorkerPath) { 52 throw new CodedError( 53 'ERR_NOTIFICATIONS_PUSH_MISSING_CONFIGURATION', 54 'You must specify `notification.serviceWorkerPath` in `app.json` to use push notifications on the web. Please provide the path to the service worker that will handle notifications.' 55 ); 56 } 57 guardPermission(); 58 59 let registration: ServiceWorkerRegistration | null = null; 60 try { 61 registration = await navigator.serviceWorker.register(serviceWorkerPath); 62 } catch (error) { 63 throw new CodedError( 64 'ERR_NOTIFICATIONS_PUSH_REGISTRATION_FAILED', 65 `Could not register this device for push notifications because the service worker (${serviceWorkerPath}) could not be registered: ${error}` 66 ); 67 } 68 await navigator.serviceWorker.ready; 69 70 if (!registration.active) { 71 throw new CodedError( 72 'ERR_NOTIFICATIONS_PUSH_REGISTRATION_FAILED', 73 'Could not register this device for push notifications because the service worker is not active.' 74 ); 75 } 76 77 const subscribeOptions = { 78 userVisibleOnly: true, 79 applicationServerKey: _urlBase64ToUint8Array(vapidPublicKey), 80 }; 81 let pushSubscription: PushSubscription | null = null; 82 try { 83 pushSubscription = await registration.pushManager.subscribe(subscribeOptions); 84 } catch (error) { 85 throw new CodedError( 86 'ERR_NOTIFICATIONS_PUSH_REGISTRATION_FAILED', 87 'The device was unable to register for remote notifications with the browser endpoint. (' + 88 error + 89 ')' 90 ); 91 } 92 const pushSubscriptionJson = pushSubscription.toJSON(); 93 94 const subscriptionObject = { 95 endpoint: pushSubscriptionJson.endpoint, 96 keys: { 97 p256dh: pushSubscriptionJson.keys!.p256dh, 98 auth: pushSubscriptionJson.keys!.auth, 99 }, 100 }; 101 102 // Store notification icon string in service worker. 103 // This message is received by `/expo-service-worker.js`. 104 // We wrap it with `fromExpoWebClient` to make sure other message 105 // will not override content such as `notificationIcon`. 106 // https://stackoverflow.com/a/35729334/2603230 107 const notificationIcon = ( 108 Constants.manifest?.notification ?? 109 Constants.manifest2?.extra?.expoClient?.notification ?? 110 {} 111 ).icon; 112 await registration.active.postMessage( 113 JSON.stringify({ fromExpoWebClient: { notificationIcon } }) 114 ); 115 116 return subscriptionObject; 117} 118 119// https://github.com/web-push-libs/web-push#using-vapid-key-for-applicationserverkey 120function _urlBase64ToUint8Array(base64String: string): Uint8Array { 121 const padding = '='.repeat((4 - (base64String.length % 4)) % 4); 122 const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); 123 124 const rawData = window.atob(base64); 125 const outputArray = new Uint8Array(rawData.length); 126 127 for (let i = 0; i < rawData.length; ++i) { 128 outputArray[i] = rawData.charCodeAt(i); 129 } 130 return outputArray; 131} 132