import * as Application from 'expo-application'; import Constants from 'expo-constants'; import { Platform, CodedError, UnavailabilityError } from 'expo-modules-core'; import { setAutoServerRegistrationEnabledAsync } from './DevicePushTokenAutoRegistration.fx'; import ServerRegistrationModule from './ServerRegistrationModule'; import { DevicePushToken, ExpoPushToken, ExpoPushTokenOptions } from './Tokens.types'; import getDevicePushTokenAsync from './getDevicePushTokenAsync'; const productionBaseUrl = 'https://exp.host/--/api/v2/'; /** * Returns an Expo token that can be used to send a push notification to the device using Expo's push notifications service. * * This method makes requests to the Expo's servers. It can get rejected in cases where the request itself fails * (for example, due to the device being offline, experiencing a network timeout, or other HTTPS request failures). * To provide offline support to your users, you should `try/catch` this method and implement retry logic to attempt * to get the push token later, once the device is back online. * * > For Expo's backend to be able to send notifications to your app, you will need to provide it with push notification keys. * For more information, see [credentials](/push-notifications/push-notifications-setup/#get-credentials-for-development-builds) in the push notifications setup. * * @param options Object allowing you to pass in push notification configuration. * @return Returns a `Promise` that resolves to an object representing acquired push token. * @header fetch * * @example * ```ts * import * as Notifications from 'expo-notifications'; * * export async function registerForPushNotificationsAsync(userId: string) { * const expoPushToken = await Notifications.getExpoPushTokenAsync({ * projectId: 'your-project-id', * }); * * await fetch('https://example.com/', { * method: 'POST', * headers: { * 'Content-Type': 'application/json', * }, * body: JSON.stringify({ * userId, * expoPushToken, * }), * }); * } * ``` */ export default async function getExpoPushTokenAsync( options: ExpoPushTokenOptions = {} ): Promise { const devicePushToken = options.devicePushToken || (await getDevicePushTokenAsync()); const deviceId = options.deviceId || (await getDeviceIdAsync()); const projectId = options.projectId || Constants.easConfig?.projectId; if (!projectId) { console.warn( 'Calling getExpoPushTokenAsync without specifying a projectId is deprecated and will no longer be supported in SDK 49+' ); } if (!projectId) { throw new CodedError( 'ERR_NOTIFICATIONS_NO_EXPERIENCE_ID', "No 'projectId' found. If 'projectId' can't be inferred from the manifest (eg. in bare workflow), you have to pass it in yourself." ); } const applicationId = options.applicationId || Application.applicationId; if (!applicationId) { throw new CodedError( 'ERR_NOTIFICATIONS_NO_APPLICATION_ID', "No applicationId found. If it can't be inferred from native configuration by expo-application, you have to pass it in yourself." ); } const type = options.type || getTypeOfToken(devicePushToken); const development = options.development || (await shouldUseDevelopmentNotificationService()); const baseUrl = options.baseUrl ?? productionBaseUrl; const url = options.url ?? `${baseUrl}push/getExpoPushToken`; const body = { type, deviceId: deviceId.toLowerCase(), development, appId: applicationId, deviceToken: getDeviceToken(devicePushToken), projectId, }; const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify(body), }).catch((error) => { throw new CodedError( 'ERR_NOTIFICATIONS_NETWORK_ERROR', `Error encountered while fetching Expo token: ${error}.` ); }); if (!response.ok) { const statusInfo = response.statusText || response.status; let body: string | undefined = undefined; try { body = await response.text(); } catch { // do nothing } throw new CodedError( 'ERR_NOTIFICATIONS_SERVER_ERROR', `Error encountered while fetching Expo token, expected an OK response, received: ${statusInfo} (body: "${body}").` ); } const expoPushToken = getExpoPushToken(await parseResponse(response)); try { if (options.url || options.baseUrl) { console.debug( `[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.` ); } else { await setAutoServerRegistrationEnabledAsync(true); } } catch (e) { console.warn( '[expo-notifications] Could not enable automatically registering new device tokens with the Expo notification service', e ); } return { type: 'expo', data: expoPushToken, }; } async function parseResponse(response: Response) { try { return await response.json(); } catch { try { throw new CodedError( 'ERR_NOTIFICATIONS_SERVER_ERROR', `Expected a JSON response from server when fetching Expo token, received body: ${JSON.stringify( await response.text() )}.` ); } catch { throw new CodedError( 'ERR_NOTIFICATIONS_SERVER_ERROR', `Expected a JSON response from server when fetching Expo token, received response: ${JSON.stringify( response )}.` ); } } } function getExpoPushToken(data: any) { if ( !data || !(typeof data === 'object') || !data.data || !(typeof data.data === 'object') || !data.data.expoPushToken || !(typeof data.data.expoPushToken === 'string') ) { throw new CodedError( 'ERR_NOTIFICATIONS_SERVER_ERROR', `Malformed response from server, expected "{ data: { expoPushToken: string } }", received: ${JSON.stringify( data, null, 2 )}.` ); } return data.data.expoPushToken as string; } // Same as in DevicePushTokenAutoRegistration async function getDeviceIdAsync() { try { if (!ServerRegistrationModule.getInstallationIdAsync) { throw new UnavailabilityError('ExpoServerRegistrationModule', 'getInstallationIdAsync'); } return await ServerRegistrationModule.getInstallationIdAsync(); } catch (e) { throw new CodedError( 'ERR_NOTIF_DEVICE_ID', `Could not have fetched installation ID of the application: ${e}.` ); } } function getDeviceToken(devicePushToken: DevicePushToken) { if (typeof devicePushToken.data === 'string') { return devicePushToken.data; } return JSON.stringify(devicePushToken.data); } // Same as in DevicePushTokenAutoRegistration async function shouldUseDevelopmentNotificationService() { if (Platform.OS === 'ios') { try { const notificationServiceEnvironment = await Application.getIosPushNotificationServiceEnvironmentAsync(); if (notificationServiceEnvironment === 'development') { return true; } } catch { // We can't do anything here, we'll fallback to false then. } } return false; } // Same as in DevicePushTokenAutoRegistration function getTypeOfToken(devicePushToken: DevicePushToken) { switch (devicePushToken.type) { case 'ios': return 'apns'; case 'android': return 'fcm'; // This probably will error on server, but let's make this function future-safe. default: return devicePushToken.type; } }