Skip to content

Commit cc2d988

Browse files
committed
feat(fcm): Support apns.live_activity_token field in FCM ApnsConfig
* feat(fcm): Support `live_activity_token` field on `ApnsConfig` * added validation * update documentation
1 parent a46086b commit cc2d988

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed

etc/firebase-admin.messaging.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export interface AndroidNotification {
5757

5858
// @public
5959
export interface ApnsConfig {
60+
live_activity_token?: string;
6061
fcmOptions?: ApnsFcmOptions;
6162
headers?: {
6263
[key: string]: string;

src/messaging/messaging-api.ts

+4
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ export interface WebpushNotification {
241241
* Apple documentation} for various headers and payload fields supported by APNs.
242242
*/
243243
export interface ApnsConfig {
244+
/**
245+
* APN `live_activity_push_to_start_token` or `live_activity_push_token` to start or update live activities.
246+
*/
247+
live_activity_token?: string;
244248
/**
245249
* A collection of APNs headers. Header values must be strings.
246250
*/

src/messaging/messaging-internal.ts

+17
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,28 @@ function validateApnsConfig(config: ApnsConfig | undefined): void {
123123
throw new FirebaseMessagingError(
124124
MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object');
125125
}
126+
validateApnsLiveActivityToken(config.live_activity_token);
126127
validateStringMap(config.headers, 'apns.headers');
127128
validateApnsPayload(config.payload);
128129
validateApnsFcmOptions(config.fcmOptions);
129130
}
130131

132+
function validateApnsLiveActivityToken(liveActivityToken: ApnsConfig['live_activity_token']): void {
133+
if (typeof liveActivityToken === 'undefined') {
134+
return;
135+
} else if (!validator.isString(liveActivityToken)) {
136+
throw new FirebaseMessagingError(
137+
MessagingClientErrorCode.INVALID_PAYLOAD,
138+
'apns.live_activity_token must be a string value',
139+
);
140+
} else if (!validator.isNonEmptyString(liveActivityToken)) {
141+
throw new FirebaseMessagingError(
142+
MessagingClientErrorCode.INVALID_PAYLOAD,
143+
'apns.live_activity_token must be a non-empty string',
144+
);
145+
}
146+
}
147+
131148
/**
132149
* Checks if the given ApnsFcmOptions object is valid.
133150
*

test/unit/messaging/messaging.spec.ts

+165
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,21 @@ describe('Messaging', () => {
17251725
});
17261726
});
17271727

1728+
const invalidApnsLiveActivityTokens: any[] = [null, NaN, 0, 1, true, false]
1729+
invalidApnsLiveActivityTokens.forEach((arg) => {
1730+
it(`should throw given invalid apns live activity token: ${JSON.stringify(arg)}`, () => {
1731+
expect(() => {
1732+
messaging.send({ apns: { live_activity_token: arg }, topic: 'test' });
1733+
}).to.throw('apns.live_activity_token must be a string value');
1734+
});
1735+
})
1736+
1737+
it('should throw given empty apns live activity token', () => {
1738+
expect(() => {
1739+
messaging.send({ apns: { live_activity_token: '' }, topic: 'test' });
1740+
}).to.throw('apns.live_activity_token must be a non-empty string');
1741+
});
1742+
17281743
const invalidApnsPayloads: any[] = [null, '', 'payload', true, 1.23];
17291744
invalidApnsPayloads.forEach((payload) => {
17301745
it(`should throw given APNS payload with invalid object: ${JSON.stringify(payload)}`, () => {
@@ -2388,6 +2403,155 @@ describe('Messaging', () => {
23882403
},
23892404
},
23902405
},
2406+
{
2407+
label: 'APNS Start LiveActivity',
2408+
req: {
2409+
apns: {
2410+
live_activity_token: 'live-activity-token',
2411+
headers:{
2412+
'apns-priority': '10'
2413+
},
2414+
payload: {
2415+
aps: {
2416+
timestamp: 1746475860808,
2417+
event: 'start',
2418+
'content-state': {
2419+
'demo': 1
2420+
},
2421+
'attributes-type': 'DemoAttributes',
2422+
'attributes': {
2423+
'demoAttribute': 1,
2424+
},
2425+
'alert': {
2426+
'title': 'test title',
2427+
'body': 'test body'
2428+
}
2429+
},
2430+
},
2431+
},
2432+
},
2433+
expectedReq: {
2434+
apns: {
2435+
live_activity_token: 'live-activity-token',
2436+
headers:{
2437+
'apns-priority': '10'
2438+
},
2439+
payload: {
2440+
aps: {
2441+
timestamp: 1746475860808,
2442+
event: 'start',
2443+
'content-state': {
2444+
'demo': 1
2445+
},
2446+
'attributes-type': 'DemoAttributes',
2447+
'attributes': {
2448+
'demoAttribute': 1,
2449+
},
2450+
'alert': {
2451+
'title': 'test title',
2452+
'body': 'test body'
2453+
}
2454+
},
2455+
},
2456+
},
2457+
},
2458+
},
2459+
{
2460+
label: 'APNS Update LiveActivity',
2461+
req: {
2462+
apns: {
2463+
live_activity_token: 'live-activity-token',
2464+
headers:{
2465+
'apns-priority': '10'
2466+
},
2467+
payload: {
2468+
aps: {
2469+
timestamp: 1746475860808,
2470+
event: 'update',
2471+
'content-state': {
2472+
'test1': 100,
2473+
'test2': 'demo'
2474+
},
2475+
'alert': {
2476+
'title': 'test title',
2477+
'body': 'test body'
2478+
}
2479+
},
2480+
},
2481+
},
2482+
},
2483+
expectedReq: {
2484+
apns: {
2485+
live_activity_token: 'live-activity-token',
2486+
headers:{
2487+
'apns-priority': '10'
2488+
},
2489+
payload: {
2490+
aps: {
2491+
timestamp: 1746475860808,
2492+
event: 'update',
2493+
'content-state': {
2494+
'test1': 100,
2495+
'test2': 'demo'
2496+
},
2497+
'alert': {
2498+
'title': 'test title',
2499+
'body': 'test body'
2500+
}
2501+
},
2502+
},
2503+
},
2504+
},
2505+
},
2506+
{
2507+
label: 'APNS End LiveActivity',
2508+
req: {
2509+
apns: {
2510+
live_activity_token: 'live-activity-token',
2511+
'headers':{
2512+
'apns-priority': '10'
2513+
},
2514+
payload: {
2515+
aps: {
2516+
timestamp: 1746475860808,
2517+
'dismissal-date': 1746475860808 + 60,
2518+
event: 'end',
2519+
'content-state': {
2520+
'test1': 100,
2521+
'test2': 'demo'
2522+
},
2523+
'alert': {
2524+
'title': 'test title',
2525+
'body': 'test body'
2526+
}
2527+
},
2528+
},
2529+
},
2530+
},
2531+
expectedReq: {
2532+
apns: {
2533+
live_activity_token: 'live-activity-token',
2534+
'headers':{
2535+
'apns-priority': '10'
2536+
},
2537+
payload: {
2538+
aps: {
2539+
timestamp: 1746475860808,
2540+
'dismissal-date': 1746475860808 + 60,
2541+
event: 'end',
2542+
'content-state': {
2543+
'test1': 100,
2544+
'test2': 'demo'
2545+
},
2546+
'alert': {
2547+
'title': 'test title',
2548+
'body': 'test body'
2549+
}
2550+
},
2551+
},
2552+
},
2553+
},
2554+
},
23912555
];
23922556

23932557
validMessages.forEach((config) => {
@@ -2404,6 +2568,7 @@ describe('Messaging', () => {
24042568
.then(() => {
24052569
const expectedReq = config.expectedReq || config.req;
24062570
expectedReq.token = 'mock-token';
2571+
24072572
expect(httpsRequestStub).to.have.been.calledOnce.and.calledWith({
24082573
method: 'POST',
24092574
data: { message: expectedReq },

0 commit comments

Comments
 (0)