Skip to content

Commit 2414645

Browse files
authored
Implemented APNs Critical Sound Support (#409)
* Implemented APNs Critical Sound Support * Adding typeguard to isString()
1 parent 42b5806 commit 2414645

File tree

5 files changed

+140
-3
lines changed

5 files changed

+140
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
- [added] `messaging.Aps` type now supports configuring a critical sound.
4+
A new `messaging.CriticalSound` type has been introduced for this purpose.
5+
- [added] `messaging.AndroidNotification` type now supports `channel_id`.
36
- [added] `AppOptions` now accepts an optional `http.Agent` object. The
47
`http.Agent` specified via this API is used when the SDK makes backend
58
HTTP calls. This can be used when it is required to deploy the Admin SDK

src/index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ declare namespace admin.messaging {
449449
type Aps = {
450450
alert?: string | ApsAlert;
451451
badge?: number;
452-
sound?: string;
452+
sound?: string | CriticalSound;
453453
contentAvailable?: boolean;
454454
mutableContent?: boolean;
455455
category?: string;
@@ -471,6 +471,12 @@ declare namespace admin.messaging {
471471
launchImage?: string;
472472
};
473473

474+
type CriticalSound = {
475+
critical?: boolean;
476+
name?: string;
477+
volume?: number;
478+
}
479+
474480
type Notification = {
475481
title?: string;
476482
body?: string;

src/messaging/messaging.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,20 @@ export interface ApnsPayload {
170170
export interface Aps {
171171
alert?: string | ApsAlert;
172172
badge?: number;
173-
sound?: string;
173+
sound?: string | CriticalSound;
174174
contentAvailable?: boolean;
175175
category?: string;
176176
threadId?: string;
177177
mutableContent?: boolean;
178178
[customData: string]: any;
179179
}
180180

181+
export interface CriticalSound {
182+
critical?: boolean;
183+
name?: string;
184+
volume?: number;
185+
}
186+
181187
export interface ApsAlert {
182188
title?: string;
183189
subtitle?: string;
@@ -299,6 +305,7 @@ function validateAps(aps: Aps) {
299305
MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps must be a non-null object');
300306
}
301307
validateApsAlert(aps.alert);
308+
validateApsSound(aps.sound);
302309

303310
const propertyMappings: {[key: string]: string} = {
304311
contentAvailable: 'content-available',
@@ -332,6 +339,40 @@ function validateAps(aps: Aps) {
332339
}
333340
}
334341

342+
function validateApsSound(sound: string | CriticalSound) {
343+
if (typeof sound === 'undefined' || validator.isString(sound)) {
344+
return;
345+
} else if (!validator.isNonNullObject(sound)) {
346+
throw new FirebaseMessagingError(
347+
MessagingClientErrorCode.INVALID_PAYLOAD,
348+
'apns.payload.aps.sound must be a string or a non-null object');
349+
}
350+
351+
const volume = sound.volume;
352+
if (typeof volume !== 'undefined') {
353+
if (!validator.isNumber(volume)) {
354+
throw new FirebaseMessagingError(
355+
MessagingClientErrorCode.INVALID_PAYLOAD,
356+
'apns.payload.aps.sound.volume must be a number');
357+
}
358+
if (volume < 0 || volume > 1) {
359+
throw new FirebaseMessagingError(
360+
MessagingClientErrorCode.INVALID_PAYLOAD,
361+
'apns.payload.aps.sound.volume must be in the interval [0, 1]');
362+
}
363+
}
364+
const soundObject = sound as {[key: string]: any};
365+
const key = 'critical';
366+
const critical = soundObject[key];
367+
if (typeof critical !== 'undefined' && critical !== 1) {
368+
if (critical === true) {
369+
soundObject[key] = 1;
370+
} else {
371+
delete soundObject[key];
372+
}
373+
}
374+
}
375+
335376
/**
336377
* Checks if the given alert object is valid. Alert could be a string or a complex object.
337378
* If specified as an object, it must have valid localization parameters. If successful, transforms

src/utils/validator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export function isNumber(value: any): boolean {
7575
* @param {any} value The value to validate.
7676
* @return {boolean} Whether the value is a string or not.
7777
*/
78-
export function isString(value: any): boolean {
78+
export function isString(value: any): value is string {
7979
return typeof value === 'string';
8080
}
8181

test/unit/messaging/messaging.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,27 @@ describe('Messaging', () => {
17191719
}).to.throw('bodyLocKey is required when specifying bodyLocArgs');
17201720
});
17211721

1722+
const invalidVolumes = [-0.1, 1.1];
1723+
invalidVolumes.forEach((volume) => {
1724+
it(`should throw given invalid apns sound volume: ${volume}`, () => {
1725+
const message: Message = {
1726+
condition: 'topic-name',
1727+
apns: {
1728+
payload: {
1729+
aps: {
1730+
sound: {
1731+
volume,
1732+
},
1733+
},
1734+
},
1735+
},
1736+
};
1737+
expect(() => {
1738+
messaging.send(message);
1739+
}).to.throw('volume must be in the interval [0, 1]');
1740+
});
1741+
});
1742+
17221743
it('should throw given apns titleLocArgs without titleLocKey', () => {
17231744
const message: Message = {
17241745
condition: 'topic-name',
@@ -1859,6 +1880,15 @@ describe('Messaging', () => {
18591880
}).to.throw('apns.payload.aps.alert must be a string or a non-null object');
18601881
});
18611882
});
1883+
1884+
const invalidApnsSounds: any[] = [null, [], true, 1.23];
1885+
invalidApnsSounds.forEach((sound) => {
1886+
it(`should throw given APNS payload with invalid aps sound: ${JSON.stringify(sound)}`, () => {
1887+
expect(() => {
1888+
messaging.send({apns: {payload: {aps: {sound}}}, token: 'token'});
1889+
}).to.throw('apns.payload.aps.sound must be a string or a non-null object');
1890+
});
1891+
});
18621892
});
18631893

18641894
describe('Options validation', () => {
@@ -2344,6 +2374,63 @@ describe('Messaging', () => {
23442374
},
23452375
},
23462376
},
2377+
{
2378+
label: 'APNS critical sound',
2379+
req: {
2380+
apns: {
2381+
payload: {
2382+
aps: {
2383+
sound: {
2384+
critical: true,
2385+
name: 'test.sound',
2386+
volume: 0.5,
2387+
},
2388+
},
2389+
},
2390+
},
2391+
},
2392+
expectedReq: {
2393+
apns: {
2394+
payload: {
2395+
aps: {
2396+
sound: {
2397+
critical: 1,
2398+
name: 'test.sound',
2399+
volume: 0.5,
2400+
},
2401+
},
2402+
},
2403+
},
2404+
},
2405+
},
2406+
{
2407+
label: 'APNS critical sound explicitly false',
2408+
req: {
2409+
apns: {
2410+
payload: {
2411+
aps: {
2412+
sound: {
2413+
critical: false,
2414+
name: 'test.sound',
2415+
volume: 0.5,
2416+
},
2417+
},
2418+
},
2419+
},
2420+
},
2421+
expectedReq: {
2422+
apns: {
2423+
payload: {
2424+
aps: {
2425+
sound: {
2426+
name: 'test.sound',
2427+
volume: 0.5,
2428+
},
2429+
},
2430+
},
2431+
},
2432+
},
2433+
},
23472434
{
23482435
label: 'APNS contentAvailable explicitly false',
23492436
req: {

0 commit comments

Comments
 (0)