@elemental-design/expo-notifications
Version:
183 lines (157 loc) • 5.16 kB
text/typescript
import { Platform, CodedError } from '@unimodules/core';
import * as Application from 'expo-application';
import Constants from 'expo-constants';
import InstallationIdProvider from './InstallationIdProvider';
import { DevicePushToken, ExpoPushToken } from './Tokens.types';
import getDevicePushTokenAsync from './getDevicePushTokenAsync';
const productionBaseUrl = 'https://exp.host/--/api/v2/';
interface Options {
// Endpoint URL override
baseUrl?: string;
// Request URL override
url?: string;
// Request body overrides
type?: string;
deviceId?: string;
development?: boolean;
experienceId?: string;
applicationId?: string;
devicePushToken?: DevicePushToken;
}
export default async function getExpoPushTokenAsync(options: Options = {}): Promise<ExpoPushToken> {
const devicePushToken = options.devicePushToken || (await getDevicePushTokenAsync());
const deviceId = options.deviceId || (await getDeviceIdAsync());
const experienceId = options.experienceId || (Constants.manifest && Constants.manifest.id);
if (!experienceId) {
throw new CodedError(
'ERR_NOTIFICATIONS_NO_EXPERIENCE_ID',
"No experienceId found. If it 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 url = options.url || `${options.baseUrl || productionBaseUrl}push/getExpoPushToken`;
const body = {
type,
deviceId,
development,
experienceId,
appId: applicationId,
deviceToken: getDeviceToken(devicePushToken),
};
const response = await fetch(url, {
method: 'POST',
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 (error) {
// 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));
return {
type: 'expo',
data: expoPushToken,
};
}
async function parseResponse(response: Response) {
try {
return await response.json();
} catch (error) {
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 (innerError) {
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;
}
async function getDeviceIdAsync() {
try {
return await InstallationIdProvider.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);
}
async function shouldUseDevelopmentNotificationService() {
if (Platform.OS === 'ios') {
try {
const notificationServiceEnvironment = await Application.getIosPushNotificationServiceEnvironmentAsync();
if (notificationServiceEnvironment === 'development') {
return true;
}
} catch (e) {
// We can't do anything here, we'll fallback to false then.
}
}
return false;
}
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;
}
}