1import * as Application from 'expo-application'; 2import Constants from 'expo-constants'; 3import { Platform, CodedError, UnavailabilityError } from 'expo-modules-core'; 4 5import { setAutoServerRegistrationEnabledAsync } from './DevicePushTokenAutoRegistration.fx'; 6import ServerRegistrationModule from './ServerRegistrationModule'; 7import { DevicePushToken, ExpoPushToken, ExpoPushTokenOptions } from './Tokens.types'; 8import getDevicePushTokenAsync from './getDevicePushTokenAsync'; 9 10const productionBaseUrl = 'https://exp.host/--/api/v2/'; 11 12/** 13 * Returns an Expo token that can be used to send a push notification to the device using Expo's push notifications service. 14 * 15 * This method makes requests to the Expo's servers. It can get rejected in cases where the request itself fails 16 * (for example, due to the device being offline, experiencing a network timeout, or other HTTPS request failures). 17 * To provide offline support to your users, you should `try/catch` this method and implement retry logic to attempt 18 * to get the push token later, once the device is back online. 19 * 20 * > For Expo's backend to be able to send notifications to your app, you will need to provide it with push notification keys. 21 * For more information, see [credentials](/push-notifications/push-notifications-setup/#get-credentials-for-development-builds) in the push notifications setup. 22 * 23 * @param options Object allowing you to pass in push notification configuration. 24 * @return Returns a `Promise` that resolves to an object representing acquired push token. 25 * @header fetch 26 * 27 * @example 28 * ```ts 29 * import * as Notifications from 'expo-notifications'; 30 * 31 * export async function registerForPushNotificationsAsync(userId: string) { 32 * const expoPushToken = await Notifications.getExpoPushTokenAsync({ 33 * projectId: 'your-project-id', 34 * }); 35 * 36 * await fetch('https://example.com/', { 37 * method: 'POST', 38 * headers: { 39 * 'Content-Type': 'application/json', 40 * }, 41 * body: JSON.stringify({ 42 * userId, 43 * expoPushToken, 44 * }), 45 * }); 46 * } 47 * ``` 48 */ 49export default async function getExpoPushTokenAsync( 50 options: ExpoPushTokenOptions = {} 51): Promise<ExpoPushToken> { 52 const devicePushToken = options.devicePushToken || (await getDevicePushTokenAsync()); 53 54 const deviceId = options.deviceId || (await getDeviceIdAsync()); 55 const projectId = options.projectId || Constants.easConfig?.projectId; 56 57 if (!projectId) { 58 console.warn( 59 'Calling getExpoPushTokenAsync without specifying a projectId is deprecated and will no longer be supported in SDK 49+' 60 ); 61 } 62 63 if (!projectId) { 64 throw new CodedError( 65 'ERR_NOTIFICATIONS_NO_EXPERIENCE_ID', 66 "No 'projectId' found. If 'projectId' can't be inferred from the manifest (eg. in bare workflow), you have to pass it in yourself." 67 ); 68 } 69 70 const applicationId = options.applicationId || Application.applicationId; 71 if (!applicationId) { 72 throw new CodedError( 73 'ERR_NOTIFICATIONS_NO_APPLICATION_ID', 74 "No applicationId found. If it can't be inferred from native configuration by expo-application, you have to pass it in yourself." 75 ); 76 } 77 const type = options.type || getTypeOfToken(devicePushToken); 78 const development = options.development || (await shouldUseDevelopmentNotificationService()); 79 80 const baseUrl = options.baseUrl ?? productionBaseUrl; 81 const url = options.url ?? `${baseUrl}push/getExpoPushToken`; 82 83 const body = { 84 type, 85 deviceId: deviceId.toLowerCase(), 86 development, 87 appId: applicationId, 88 deviceToken: getDeviceToken(devicePushToken), 89 projectId, 90 }; 91 92 const response = await fetch(url, { 93 method: 'POST', 94 headers: { 95 'content-type': 'application/json', 96 }, 97 body: JSON.stringify(body), 98 }).catch((error) => { 99 throw new CodedError( 100 'ERR_NOTIFICATIONS_NETWORK_ERROR', 101 `Error encountered while fetching Expo token: ${error}.` 102 ); 103 }); 104 105 if (!response.ok) { 106 const statusInfo = response.statusText || response.status; 107 let body: string | undefined = undefined; 108 try { 109 body = await response.text(); 110 } catch { 111 // do nothing 112 } 113 throw new CodedError( 114 'ERR_NOTIFICATIONS_SERVER_ERROR', 115 `Error encountered while fetching Expo token, expected an OK response, received: ${statusInfo} (body: "${body}").` 116 ); 117 } 118 119 const expoPushToken = getExpoPushToken(await parseResponse(response)); 120 121 try { 122 if (options.url || options.baseUrl) { 123 console.debug( 124 `[expo-notifications] Since the URL endpoint to register in has been customized in the options, expo-notifications won't try to auto-update the device push token on the server.` 125 ); 126 } else { 127 await setAutoServerRegistrationEnabledAsync(true); 128 } 129 } catch (e) { 130 console.warn( 131 '[expo-notifications] Could not enable automatically registering new device tokens with the Expo notification service', 132 e 133 ); 134 } 135 136 return { 137 type: 'expo', 138 data: expoPushToken, 139 }; 140} 141 142async function parseResponse(response: Response) { 143 try { 144 return await response.json(); 145 } catch { 146 try { 147 throw new CodedError( 148 'ERR_NOTIFICATIONS_SERVER_ERROR', 149 `Expected a JSON response from server when fetching Expo token, received body: ${JSON.stringify( 150 await response.text() 151 )}.` 152 ); 153 } catch { 154 throw new CodedError( 155 'ERR_NOTIFICATIONS_SERVER_ERROR', 156 `Expected a JSON response from server when fetching Expo token, received response: ${JSON.stringify( 157 response 158 )}.` 159 ); 160 } 161 } 162} 163 164function getExpoPushToken(data: any) { 165 if ( 166 !data || 167 !(typeof data === 'object') || 168 !data.data || 169 !(typeof data.data === 'object') || 170 !data.data.expoPushToken || 171 !(typeof data.data.expoPushToken === 'string') 172 ) { 173 throw new CodedError( 174 'ERR_NOTIFICATIONS_SERVER_ERROR', 175 `Malformed response from server, expected "{ data: { expoPushToken: string } }", received: ${JSON.stringify( 176 data, 177 null, 178 2 179 )}.` 180 ); 181 } 182 183 return data.data.expoPushToken as string; 184} 185 186// Same as in DevicePushTokenAutoRegistration 187async function getDeviceIdAsync() { 188 try { 189 if (!ServerRegistrationModule.getInstallationIdAsync) { 190 throw new UnavailabilityError('ExpoServerRegistrationModule', 'getInstallationIdAsync'); 191 } 192 193 return await ServerRegistrationModule.getInstallationIdAsync(); 194 } catch (e) { 195 throw new CodedError( 196 'ERR_NOTIF_DEVICE_ID', 197 `Could not have fetched installation ID of the application: ${e}.` 198 ); 199 } 200} 201 202function getDeviceToken(devicePushToken: DevicePushToken) { 203 if (typeof devicePushToken.data === 'string') { 204 return devicePushToken.data; 205 } 206 207 return JSON.stringify(devicePushToken.data); 208} 209 210// Same as in DevicePushTokenAutoRegistration 211async function shouldUseDevelopmentNotificationService() { 212 if (Platform.OS === 'ios') { 213 try { 214 const notificationServiceEnvironment = 215 await Application.getIosPushNotificationServiceEnvironmentAsync(); 216 if (notificationServiceEnvironment === 'development') { 217 return true; 218 } 219 } catch { 220 // We can't do anything here, we'll fallback to false then. 221 } 222 } 223 224 return false; 225} 226 227// Same as in DevicePushTokenAutoRegistration 228function getTypeOfToken(devicePushToken: DevicePushToken) { 229 switch (devicePushToken.type) { 230 case 'ios': 231 return 'apns'; 232 case 'android': 233 return 'fcm'; 234 // This probably will error on server, but let's make this function future-safe. 235 default: 236 return devicePushToken.type; 237 } 238} 239