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