From 6b5079e5277f8244e70f98edb1426a5991384278 Mon Sep 17 00:00:00 2001 From: Jano Detzel Date: Thu, 3 Apr 2025 15:26:58 +0200 Subject: [PATCH 1/2] feat(fcm): Support `apns.live_activity_token` field in FCM `ApnsConfig` * feat(fcm): Support `live_activity_token` field on `ApnsConfig` * added validation * update documentation --- etc/firebase-admin.messaging.api.md | 1 + src/messaging/messaging-api.ts | 4 + src/messaging/messaging-internal.ts | 17 +++ test/unit/messaging/messaging.spec.ts | 165 ++++++++++++++++++++++++++ 4 files changed, 187 insertions(+) diff --git a/etc/firebase-admin.messaging.api.md b/etc/firebase-admin.messaging.api.md index f0adaa0f35..0a3bab53a2 100644 --- a/etc/firebase-admin.messaging.api.md +++ b/etc/firebase-admin.messaging.api.md @@ -61,6 +61,7 @@ export interface ApnsConfig { headers?: { [key: string]: string; }; + live_activity_token?: string; payload?: ApnsPayload; } diff --git a/src/messaging/messaging-api.ts b/src/messaging/messaging-api.ts index 69c16382d5..2cb21caad2 100644 --- a/src/messaging/messaging-api.ts +++ b/src/messaging/messaging-api.ts @@ -241,6 +241,10 @@ export interface WebpushNotification { * Apple documentation} for various headers and payload fields supported by APNs. */ export interface ApnsConfig { + /** + * APN `live_activity_push_to_start_token` or `live_activity_push_token` to start or update live activities. + */ + live_activity_token?: string; /** * A collection of APNs headers. Header values must be strings. */ diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts index 725769bb32..4dd11c3545 100644 --- a/src/messaging/messaging-internal.ts +++ b/src/messaging/messaging-internal.ts @@ -123,11 +123,28 @@ function validateApnsConfig(config: ApnsConfig | undefined): void { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object'); } + validateApnsLiveActivityToken(config.live_activity_token); validateStringMap(config.headers, 'apns.headers'); validateApnsPayload(config.payload); validateApnsFcmOptions(config.fcmOptions); } +function validateApnsLiveActivityToken(liveActivityToken: ApnsConfig['live_activity_token']): void { + if (typeof liveActivityToken === 'undefined') { + return; + } else if (!validator.isString(liveActivityToken)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.live_activity_token must be a string value', + ); + } else if (!validator.isNonEmptyString(liveActivityToken)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_PAYLOAD, + 'apns.live_activity_token must be a non-empty string', + ); + } +} + /** * Checks if the given ApnsFcmOptions object is valid. * diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 79942f73d2..a16369261a 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -1725,6 +1725,21 @@ describe('Messaging', () => { }); }); + const invalidApnsLiveActivityTokens: any[] = [null, NaN, 0, 1, true, false] + invalidApnsLiveActivityTokens.forEach((arg) => { + it(`should throw given invalid apns live activity token: ${JSON.stringify(arg)}`, () => { + expect(() => { + messaging.send({ apns: { live_activity_token: arg }, topic: 'test' }); + }).to.throw('apns.live_activity_token must be a string value'); + }); + }) + + it('should throw given empty apns live activity token', () => { + expect(() => { + messaging.send({ apns: { live_activity_token: '' }, topic: 'test' }); + }).to.throw('apns.live_activity_token must be a non-empty string'); + }); + const invalidApnsPayloads: any[] = [null, '', 'payload', true, 1.23]; invalidApnsPayloads.forEach((payload) => { it(`should throw given APNS payload with invalid object: ${JSON.stringify(payload)}`, () => { @@ -2388,6 +2403,155 @@ describe('Messaging', () => { }, }, }, + { + label: 'APNS Start LiveActivity', + req: { + apns: { + live_activity_token: 'live-activity-token', + headers:{ + 'apns-priority': '10' + }, + payload: { + aps: { + timestamp: 1746475860808, + event: 'start', + 'content-state': { + 'demo': 1 + }, + 'attributes-type': 'DemoAttributes', + 'attributes': { + 'demoAttribute': 1, + }, + 'alert': { + 'title': 'test title', + 'body': 'test body' + } + }, + }, + }, + }, + expectedReq: { + apns: { + live_activity_token: 'live-activity-token', + headers:{ + 'apns-priority': '10' + }, + payload: { + aps: { + timestamp: 1746475860808, + event: 'start', + 'content-state': { + 'demo': 1 + }, + 'attributes-type': 'DemoAttributes', + 'attributes': { + 'demoAttribute': 1, + }, + 'alert': { + 'title': 'test title', + 'body': 'test body' + } + }, + }, + }, + }, + }, + { + label: 'APNS Update LiveActivity', + req: { + apns: { + live_activity_token: 'live-activity-token', + headers:{ + 'apns-priority': '10' + }, + payload: { + aps: { + timestamp: 1746475860808, + event: 'update', + 'content-state': { + 'test1': 100, + 'test2': 'demo' + }, + 'alert': { + 'title': 'test title', + 'body': 'test body' + } + }, + }, + }, + }, + expectedReq: { + apns: { + live_activity_token: 'live-activity-token', + headers:{ + 'apns-priority': '10' + }, + payload: { + aps: { + timestamp: 1746475860808, + event: 'update', + 'content-state': { + 'test1': 100, + 'test2': 'demo' + }, + 'alert': { + 'title': 'test title', + 'body': 'test body' + } + }, + }, + }, + }, + }, + { + label: 'APNS End LiveActivity', + req: { + apns: { + live_activity_token: 'live-activity-token', + 'headers':{ + 'apns-priority': '10' + }, + payload: { + aps: { + timestamp: 1746475860808, + 'dismissal-date': 1746475860808 + 60, + event: 'end', + 'content-state': { + 'test1': 100, + 'test2': 'demo' + }, + 'alert': { + 'title': 'test title', + 'body': 'test body' + } + }, + }, + }, + }, + expectedReq: { + apns: { + live_activity_token: 'live-activity-token', + 'headers':{ + 'apns-priority': '10' + }, + payload: { + aps: { + timestamp: 1746475860808, + 'dismissal-date': 1746475860808 + 60, + event: 'end', + 'content-state': { + 'test1': 100, + 'test2': 'demo' + }, + 'alert': { + 'title': 'test title', + 'body': 'test body' + } + }, + }, + }, + }, + }, ]; validMessages.forEach((config) => { @@ -2404,6 +2568,7 @@ describe('Messaging', () => { .then(() => { const expectedReq = config.expectedReq || config.req; expectedReq.token = 'mock-token'; + expect(httpsRequestStub).to.have.been.calledOnce.and.calledWith({ method: 'POST', data: { message: expectedReq }, From 3d2d1a002b80ad712466010f565f6f30ee598b90 Mon Sep 17 00:00:00 2001 From: Jano Detzel Date: Fri, 30 May 2025 22:35:26 +0200 Subject: [PATCH 2/2] feat(fcm): Rename `apns.live_activity_token` to lowerCamelCase --- etc/firebase-admin.messaging.api.md | 2 +- src/messaging/messaging-api.ts | 4 ++-- src/messaging/messaging-internal.ts | 14 ++++++++++---- test/unit/messaging/messaging.spec.ts | 14 +++++++------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/etc/firebase-admin.messaging.api.md b/etc/firebase-admin.messaging.api.md index 0a3bab53a2..1ad50f85f6 100644 --- a/etc/firebase-admin.messaging.api.md +++ b/etc/firebase-admin.messaging.api.md @@ -61,7 +61,7 @@ export interface ApnsConfig { headers?: { [key: string]: string; }; - live_activity_token?: string; + liveActivityToken?: string; payload?: ApnsPayload; } diff --git a/src/messaging/messaging-api.ts b/src/messaging/messaging-api.ts index 2cb21caad2..d0c5d5af7e 100644 --- a/src/messaging/messaging-api.ts +++ b/src/messaging/messaging-api.ts @@ -242,9 +242,9 @@ export interface WebpushNotification { */ export interface ApnsConfig { /** - * APN `live_activity_push_to_start_token` or `live_activity_push_token` to start or update live activities. + * APN `pushToStartToken` or `pushToken` to start or update live activities. */ - live_activity_token?: string; + liveActivityToken?: string; /** * A collection of APNs headers. Header values must be strings. */ diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts index 4dd11c3545..c41f93a923 100644 --- a/src/messaging/messaging-internal.ts +++ b/src/messaging/messaging-internal.ts @@ -123,24 +123,30 @@ function validateApnsConfig(config: ApnsConfig | undefined): void { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object'); } - validateApnsLiveActivityToken(config.live_activity_token); + validateApnsLiveActivityToken(config.liveActivityToken); validateStringMap(config.headers, 'apns.headers'); validateApnsPayload(config.payload); validateApnsFcmOptions(config.fcmOptions); + + const propertyMappings = { + liveActivityToken: 'live_activity_token' + } + + renameProperties(config, propertyMappings) } -function validateApnsLiveActivityToken(liveActivityToken: ApnsConfig['live_activity_token']): void { +function validateApnsLiveActivityToken(liveActivityToken: ApnsConfig['liveActivityToken']): void { if (typeof liveActivityToken === 'undefined') { return; } else if (!validator.isString(liveActivityToken)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.live_activity_token must be a string value', + 'apns.liveActivityToken must be a string value', ); } else if (!validator.isNonEmptyString(liveActivityToken)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - 'apns.live_activity_token must be a non-empty string', + 'apns.liveActivityToken must be a non-empty string', ); } } diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index a16369261a..9e8d1ea814 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -1729,15 +1729,15 @@ describe('Messaging', () => { invalidApnsLiveActivityTokens.forEach((arg) => { it(`should throw given invalid apns live activity token: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({ apns: { live_activity_token: arg }, topic: 'test' }); - }).to.throw('apns.live_activity_token must be a string value'); + messaging.send({ apns: { liveActivityToken: arg }, topic: 'test' }); + }).to.throw('apns.liveActivityToken must be a string value'); }); }) it('should throw given empty apns live activity token', () => { expect(() => { - messaging.send({ apns: { live_activity_token: '' }, topic: 'test' }); - }).to.throw('apns.live_activity_token must be a non-empty string'); + messaging.send({ apns: { liveActivityToken: '' }, topic: 'test' }); + }).to.throw('apns.liveActivityToken must be a non-empty string'); }); const invalidApnsPayloads: any[] = [null, '', 'payload', true, 1.23]; @@ -2407,7 +2407,7 @@ describe('Messaging', () => { label: 'APNS Start LiveActivity', req: { apns: { - live_activity_token: 'live-activity-token', + liveActivityToken: 'live-activity-token', headers:{ 'apns-priority': '10' }, @@ -2460,7 +2460,7 @@ describe('Messaging', () => { label: 'APNS Update LiveActivity', req: { apns: { - live_activity_token: 'live-activity-token', + liveActivityToken: 'live-activity-token', headers:{ 'apns-priority': '10' }, @@ -2507,7 +2507,7 @@ describe('Messaging', () => { label: 'APNS End LiveActivity', req: { apns: { - live_activity_token: 'live-activity-token', + liveActivityToken: 'live-activity-token', 'headers':{ 'apns-priority': '10' },