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  // @ts-expect-error: TODO: not on the schema
35  const vapidPublicKey: string | null = Constants.expoConfig?.notification?.vapidPublicKey;
36  if (!vapidPublicKey) {
37    throw new CodedError(
38      'ERR_NOTIFICATIONS_PUSH_WEB_MISSING_CONFIG',
39      '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/.'
40    );
41  }
42
43  // @ts-expect-error: TODO: not on the schema
44  const serviceWorkerPath = Constants.expoConfig?.notification?.serviceWorkerPath;
45  if (!serviceWorkerPath) {
46    throw new CodedError(
47      'ERR_NOTIFICATIONS_PUSH_MISSING_CONFIGURATION',
48      '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.'
49    );
50  }
51  guardPermission();
52
53  let registration: ServiceWorkerRegistration | null = null;
54  try {
55    registration = await navigator.serviceWorker.register(serviceWorkerPath);
56  } catch (error) {
57    throw new CodedError(
58      'ERR_NOTIFICATIONS_PUSH_REGISTRATION_FAILED',
59      `Could not register this device for push notifications because the service worker (${serviceWorkerPath}) could not be registered: ${error}`
60    );
61  }
62  await navigator.serviceWorker.ready;
63
64  if (!registration.active) {
65    throw new CodedError(
66      'ERR_NOTIFICATIONS_PUSH_REGISTRATION_FAILED',
67      'Could not register this device for push notifications because the service worker is not active.'
68    );
69  }
70
71  const subscribeOptions = {
72    userVisibleOnly: true,
73    applicationServerKey: _urlBase64ToUint8Array(vapidPublicKey),
74  };
75  let pushSubscription: PushSubscription | null = null;
76  try {
77    pushSubscription = await registration.pushManager.subscribe(subscribeOptions);
78  } catch (error) {
79    throw new CodedError(
80      'ERR_NOTIFICATIONS_PUSH_REGISTRATION_FAILED',
81      'The device was unable to register for remote notifications with the browser endpoint. (' +
82        error +
83        ')'
84    );
85  }
86  const pushSubscriptionJson = pushSubscription.toJSON();
87
88  const subscriptionObject = {
89    endpoint: pushSubscriptionJson.endpoint,
90    keys: {
91      p256dh: pushSubscriptionJson.keys!.p256dh,
92      auth: pushSubscriptionJson.keys!.auth,
93    },
94  };
95
96  // Store notification icon string in service worker.
97  // This message is received by `/expo-service-worker.js`.
98  // We wrap it with `fromExpoWebClient` to make sure other message
99  // will not override content such as `notificationIcon`.
100  // https://stackoverflow.com/a/35729334/2603230
101  const notificationIcon = (Constants.expoConfig?.notification ?? {}).icon;
102  await registration.active.postMessage(
103    JSON.stringify({ fromExpoWebClient: { notificationIcon } })
104  );
105
106  return subscriptionObject;
107}
108
109// https://github.com/web-push-libs/web-push#using-vapid-key-for-applicationserverkey
110function _urlBase64ToUint8Array(base64String: string): Uint8Array {
111  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
112  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
113
114  const rawData = window.atob(base64);
115  const outputArray = new Uint8Array(rawData.length);
116
117  for (let i = 0; i < rawData.length; ++i) {
118    outputArray[i] = rawData.charCodeAt(i);
119  }
120  return outputArray;
121}
122