Skip to content

Commit

Permalink
Added support for FCM V1 configuration (#60)
Browse files Browse the repository at this point in the history
Based on this FCM related blogpost here, [https://expo.dev/blog/expo-adds-support-for-fcm-http-v1-api](url) it seems like there's no solution for people like us who use the Node SDK to send push notifications.

This change adds support for initiating `ExpoClient` with another optional parameter called `useFCM: true` which appends the necessary query string to the `/push/send` (?useFcmV1=true)

---------

Co-authored-by: James Ide <ide@users.noreply.github.com>
  • Loading branch information
edi and ide authored Mar 20, 2024
1 parent f5e56d8 commit 5a3f2e3
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 3 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import { Expo } from 'expo-server-sdk';

// Create a new Expo SDK client
// optionally providing an access token if you have enabled push security
let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN });
let expo = new Expo({
accessToken: process.env.EXPO_ACCESS_TOKEN,
useFcmV1: false // this can be set to true in order to use the FCM v1 API
});

// Create the messages that you want to send to clients
let messages = [];
Expand Down
11 changes: 9 additions & 2 deletions src/ExpoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class Expo {
private httpAgent: Agent | undefined;
private limitConcurrentRequests: <T>(thunk: () => Promise<T>) => Promise<T>;
private accessToken: string | undefined;
private useFcmV1: boolean | undefined;

constructor(options: ExpoClientOptions = {}) {
this.httpAgent = options.httpAgent;
Expand All @@ -49,6 +50,7 @@ export class Expo {
: DEFAULT_CONCURRENT_REQUEST_LIMIT
);
this.accessToken = options.accessToken;
this.useFcmV1 = options.useFcmV1;
}

/**
Expand All @@ -75,13 +77,17 @@ export class Expo {
* sized chunks.
*/
async sendPushNotificationsAsync(messages: ExpoPushMessage[]): Promise<ExpoPushTicket[]> {
// @ts-expect-error We don't yet have type declarations for URL
const url = new URL(`${BASE_API_URL}/push/send`);
if (typeof this.useFcmV1 === 'boolean') {
url.searchParams.append('useFcmV1', this.useFcmV1);
}
const actualMessagesCount = Expo._getActualMessageCount(messages);

const data = await this.limitConcurrentRequests(async () => {
return await promiseRetry(
async (retry): Promise<any> => {
try {
return await this.requestAsync(`${BASE_API_URL}/push/send`, {
return await this.requestAsync(url.toString(), {
httpMethod: 'post',
body: messages,
shouldCompress(body) {
Expand Down Expand Up @@ -353,6 +359,7 @@ export type ExpoClientOptions = {
httpAgent?: Agent;
maxConcurrentRequests?: number;
accessToken?: string;
useFcmV1?: boolean;
};

export type ExpoPushToken = string;
Expand Down
28 changes: 28 additions & 0 deletions src/__tests__/ExpoClient-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ describe('sending push notification messages', () => {
expect(options.headers.get('Authorization')).toContain('Bearer foobar');
});

describe('the useFcmV1 option', () => {
beforeEach(() => {
(fetch as any).any({ data: [{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' }] });
});

test('sends requests to the Expo API server without the useFcmV1 parameter', async () => {
const client = new ExpoClient();
await client.sendPushNotificationsAsync([{ to: 'a' }]);
expect((fetch as any).called('https://exp.host/--/api/v2/push/send')).toBe(true);
});

test('sends requests to the Expo API server with useFcmV1=true', async () => {
const client = new ExpoClient({ useFcmV1: true });
await client.sendPushNotificationsAsync([{ to: 'a' }]);
expect((fetch as any).called('https://exp.host/--/api/v2/push/send?useFcmV1=true')).toBe(
true
);
});

test('sends requests to the Expo API server with useFcmV1=false', async () => {
const client = new ExpoClient({ useFcmV1: false });
await client.sendPushNotificationsAsync([{ to: 'a' }]);
expect((fetch as any).called('https://exp.host/--/api/v2/push/send?useFcmV1=false')).toBe(
true
);
});
});

test('compresses request bodies over 1 KiB', async () => {
const mockTickets = [{ status: 'ok', id: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' }];
(fetch as any).mock('https://exp.host/--/api/v2/push/send', { data: mockTickets });
Expand Down

0 comments on commit 5a3f2e3

Please sign in to comment.