1import { computeNextBackoffInterval } from '@ide/backoff'; 2import * as Application from 'expo-application'; 3import { CodedError, Platform, UnavailabilityError } from 'expo-modules-core'; 4 5import ServerRegistrationModule from '../ServerRegistrationModule'; 6import { DevicePushToken } from '../Tokens.types'; 7 8const updateDevicePushTokenUrl = 'https://exp.host/--/api/v2/push/updateDeviceToken'; 9 10export async function updateDevicePushTokenAsync(signal: AbortSignal, token: DevicePushToken) { 11 const doUpdateDevicePushTokenAsync = async (retry: () => void) => { 12 const [development, deviceId] = await Promise.all([ 13 shouldUseDevelopmentNotificationService(), 14 getDeviceIdAsync(), 15 ]); 16 const body = { 17 deviceId: deviceId.toLowerCase(), 18 development, 19 deviceToken: token.data, 20 appId: Application.applicationId, 21 type: getTypeOfToken(token), 22 }; 23 24 try { 25 const response = await fetch(updateDevicePushTokenUrl, { 26 method: 'POST', 27 headers: { 28 'content-type': 'application/json', 29 }, 30 body: JSON.stringify(body), 31 signal, 32 }); 33 34 // Help debug erroring servers 35 if (!response.ok) { 36 console.debug( 37 '[expo-notifications] Error encountered while updating the device push token with the server:', 38 await response.text() 39 ); 40 } 41 42 // Retry if request failed 43 if (!response.ok) { 44 retry(); 45 } 46 } catch (e) { 47 // Error returned if the request is aborted should be an 'AbortError'. In 48 // React Native fetch is polyfilled using `whatwg-fetch` which: 49 // - creates `AbortError`s like this 50 // https://github.com/github/fetch/blob/75d9455d380f365701151f3ac85c5bda4bbbde76/fetch.js#L505 51 // - which creates exceptions like 52 // https://github.com/github/fetch/blob/75d9455d380f365701151f3ac85c5bda4bbbde76/fetch.js#L490-L494 53 if (e.name === 'AbortError') { 54 // We don't consider AbortError a failure, it's a sign somewhere else the 55 // request is expected to succeed and we don't need this one, so let's 56 // just return. 57 return; 58 } 59 60 console.warn( 61 '[expo-notifications] Error thrown while updating the device push token with the server:', 62 e 63 ); 64 65 retry(); 66 } 67 }; 68 69 let shouldTry = true; 70 const retry = () => { 71 shouldTry = true; 72 }; 73 74 let retriesCount = 0; 75 const initialBackoff = 500; // 0.5 s 76 const backoffOptions = { 77 maxBackoff: 2 * 60 * 1000, // 2 minutes 78 }; 79 let nextBackoffInterval = computeNextBackoffInterval( 80 initialBackoff, 81 retriesCount, 82 backoffOptions 83 ); 84 85 while (shouldTry && !signal.aborted) { 86 // Will be set to true by `retry` if it's called 87 shouldTry = false; 88 await doUpdateDevicePushTokenAsync(retry); 89 90 // Do not wait if we won't retry 91 if (shouldTry && !signal.aborted) { 92 nextBackoffInterval = computeNextBackoffInterval( 93 initialBackoff, 94 retriesCount, 95 backoffOptions 96 ); 97 retriesCount += 1; 98 await new Promise((resolve) => setTimeout(resolve, nextBackoffInterval)); 99 } 100 } 101} 102 103// Same as in getExpoPushTokenAsync 104async function getDeviceIdAsync() { 105 try { 106 if (!ServerRegistrationModule.getInstallationIdAsync) { 107 throw new UnavailabilityError('ExpoServerRegistrationModule', 'getInstallationIdAsync'); 108 } 109 110 return await ServerRegistrationModule.getInstallationIdAsync(); 111 } catch (e) { 112 throw new CodedError( 113 'ERR_NOTIFICATIONS_DEVICE_ID', 114 `Could not fetch the installation ID of the application: ${e}.` 115 ); 116 } 117} 118 119// Same as in getExpoPushTokenAsync 120function getTypeOfToken(devicePushToken: DevicePushToken) { 121 switch (devicePushToken.type) { 122 case 'ios': 123 return 'apns'; 124 case 'android': 125 return 'fcm'; 126 // This probably will error on server, but let's make this function future-safe. 127 default: 128 return devicePushToken.type; 129 } 130} 131 132// Same as in getExpoPushTokenAsync 133async function shouldUseDevelopmentNotificationService() { 134 if (Platform.OS === 'ios') { 135 try { 136 const notificationServiceEnvironment = 137 await Application.getIosPushNotificationServiceEnvironmentAsync(); 138 if (notificationServiceEnvironment === 'development') { 139 return true; 140 } 141 } catch { 142 // We can't do anything here, we'll fallback to false then. 143 } 144 } 145 146 return false; 147} 148