Skip to content

Commit f13388f

Browse files
feat(fcm): Add image in notification support (#648)
* feat(fcm): Add image in notification support - Add image in notification support Testing: - Add unit tests and modified integration test to reflect this change API Changes: - In Firebase Cloud Messaging, added 'image' field in the general Notification class, the AndroidNotification class, and the ApnsFcmOptions class. RELEASE NOTE: Added new APIs for specifying an image URL in notifications. * PR fixes * Update notification api
1 parent 2d2ed53 commit f13388f

File tree

4 files changed

+94
-0
lines changed

4 files changed

+94
-0
lines changed

src/index.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3923,6 +3923,11 @@ declare namespace admin.messaging {
39233923
*/
39243924
tag?: string;
39253925

3926+
/**
3927+
* URL of an image to be displayed in the notification.
3928+
*/
3929+
imageUrl?: string;
3930+
39263931
/**
39273932
* Action associated with a user click on the notification. If specified, an
39283933
* activity with a matching Intent Filter is launched when a user clicks on the
@@ -4105,6 +4110,11 @@ declare namespace admin.messaging {
41054110
* The label associated with the message's analytics data.
41064111
*/
41074112
analyticsLabel?: string;
4113+
4114+
/**
4115+
* URL of an image to be displayed in the notification.
4116+
*/
4117+
imageUrl?: string;
41084118
}
41094119

41104120
/**
@@ -4131,6 +4141,10 @@ declare namespace admin.messaging {
41314141
* The notification body
41324142
*/
41334143
body?: string;
4144+
/**
4145+
* URL of an image to be displayed in the notification.
4146+
*/
4147+
imageUrl?: string;
41344148
}
41354149
/**
41364150
* Represents the WebPush protocol options that can be included in an

src/messaging/messaging-types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface MulticastMessage extends BaseMessage {
5959
export interface Notification {
6060
title?: string;
6161
body?: string;
62+
imageUrl?: string;
6263
}
6364

6465
export interface FcmOptions {
@@ -143,6 +144,7 @@ export interface ApsAlert {
143144

144145
export interface ApnsFcmOptions {
145146
analyticsLabel?: string;
147+
imageUrl?: string;
146148
}
147149

148150
export interface AndroidConfig {
@@ -162,6 +164,7 @@ export interface AndroidNotification {
162164
color?: string;
163165
sound?: string;
164166
tag?: string;
167+
imageUrl?: string;
165168
clickAction?: string;
166169
bodyLocKey?: string;
167170
bodyLocArgs?: string[];
@@ -307,6 +310,7 @@ export function validateMessage(message: Message) {
307310
validateWebpushConfig(message.webpush);
308311
validateApnsConfig(message.apns);
309312
validateFcmOptions(message.fcmOptions);
313+
validateNotification(message.notification);
310314
}
311315

312316
/**
@@ -377,6 +381,13 @@ function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions) {
377381
MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object');
378382
}
379383

384+
if (typeof fcmOptions.imageUrl !== 'undefined' &&
385+
!validator.isURL(fcmOptions.imageUrl)) {
386+
throw new FirebaseMessagingError(
387+
MessagingClientErrorCode.INVALID_PAYLOAD,
388+
'imageUrl must be a valid URL string');
389+
}
390+
380391
if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) {
381392
throw new FirebaseMessagingError(
382393
MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value');
@@ -402,7 +413,24 @@ function validateFcmOptions(fcmOptions: FcmOptions) {
402413
}
403414
}
404415

416+
/**
417+
* Checks if the given Notification object is valid.
418+
*
419+
* @param {Notification} notification An object to be validated.
420+
*/
421+
function validateNotification(notification: Notification) {
422+
if (typeof notification === 'undefined') {
423+
return;
424+
} else if (!validator.isNonNullObject(notification)) {
425+
throw new FirebaseMessagingError(
426+
MessagingClientErrorCode.INVALID_PAYLOAD, 'notification must be a non-null object');
427+
}
405428

429+
if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) {
430+
throw new FirebaseMessagingError(
431+
MessagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string');
432+
}
433+
}
406434

407435
/**
408436
* Checks if the given ApnsPayload object is valid. The object must have a valid aps value.
@@ -632,6 +660,12 @@ function validateAndroidNotification(notification: AndroidNotification) {
632660
MessagingClientErrorCode.INVALID_PAYLOAD,
633661
'android.notification.titleLocKey is required when specifying titleLocArgs');
634662
}
663+
if (typeof notification.imageUrl !== 'undefined' &&
664+
!validator.isURL(notification.imageUrl)) {
665+
throw new FirebaseMessagingError(
666+
MessagingClientErrorCode.INVALID_PAYLOAD,
667+
'android.notification.imageUrl must be a valid URL string');
668+
}
635669

636670
const propertyMappings = {
637671
clickAction: 'click_action',

test/integration/messaging.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const message: admin.messaging.Message = {
4545
notification: {
4646
title: 'Message title',
4747
body: 'Message body',
48+
imageUrl: 'https://example.com/image.png',
4849
},
4950
android: {
5051
restrictedPackageName: 'com.google.firebase.testing',

test/unit/messaging/messaging.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2302,6 +2302,21 @@ describe('Messaging', () => {
23022302
});
23032303
});
23042304

2305+
const invalidImages = ['', 'a', 'foo', 'image.jpg'];
2306+
invalidImages.forEach((imageUrl) => {
2307+
it(`should throw given an invalid imageUrl: ${imageUrl}`, () => {
2308+
const message: Message = {
2309+
condition: 'topic-name',
2310+
notification: {
2311+
imageUrl,
2312+
},
2313+
};
2314+
expect(() => {
2315+
messaging.send(message);
2316+
}).to.throw('notification.imageUrl must be a valid URL string');
2317+
});
2318+
});
2319+
23052320
const invalidTtls = ['', 'abc', '123', '-123s', '1.2.3s', 'As', 's', '1s', -1];
23062321
invalidTtls.forEach((ttl) => {
23072322
it(`should throw given an invalid ttl: ${ ttl }`, () => {
@@ -2334,6 +2349,22 @@ describe('Messaging', () => {
23342349
});
23352350
});
23362351

2352+
invalidImages.forEach((imageUrl) => {
2353+
it(`should throw given an invalid imageUrl: ${ imageUrl }`, () => {
2354+
const message: Message = {
2355+
condition: 'topic-name',
2356+
android: {
2357+
notification: {
2358+
imageUrl,
2359+
},
2360+
},
2361+
};
2362+
expect(() => {
2363+
messaging.send(message);
2364+
}).to.throw('android.notification.imageUrl must be a valid URL string');
2365+
});
2366+
});
2367+
23372368
it('should throw given android titleLocArgs without titleLocKey', () => {
23382369
const message: Message = {
23392370
condition: 'topic-name',
@@ -2489,6 +2520,14 @@ describe('Messaging', () => {
24892520
});
24902521
});
24912522

2523+
invalidImages.forEach((imageUrl) => {
2524+
it(`should throw given invalid URL string for imageUrl`, () => {
2525+
expect(() => {
2526+
messaging.send({apns: {fcmOptions: {imageUrl}}, topic: 'test'});
2527+
}).to.throw('imageUrl must be a valid URL string');
2528+
});
2529+
});
2530+
24922531
const invalidDataMessages: any[] = [
24932532
{label: 'data', message: {data: {k1: true}}},
24942533
{label: 'android.data', message: {android: {data: {k1: true}}}},
@@ -2765,6 +2804,7 @@ describe('Messaging', () => {
27652804
notification: {
27662805
title: 'test.title',
27672806
body: 'test.body',
2807+
image: 'https://example.com/image.png',
27682808
},
27692809
},
27702810
},
@@ -2798,6 +2838,7 @@ describe('Messaging', () => {
27982838
color: '#112233',
27992839
sound: 'test.sound',
28002840
tag: 'test.tag',
2841+
image: 'https://example.com/image.png',
28012842
},
28022843
},
28032844
},
@@ -2871,6 +2912,7 @@ describe('Messaging', () => {
28712912
color: '#112233',
28722913
sound: 'test.sound',
28732914
tag: 'test.tag',
2915+
image: 'https://example.com/image.png',
28742916
clickAction: 'test.click.action',
28752917
titleLocKey: 'title.loc.key',
28762918
titleLocArgs: ['arg1', 'arg2'],
@@ -2900,6 +2942,7 @@ describe('Messaging', () => {
29002942
color: '#112233',
29012943
sound: 'test.sound',
29022944
tag: 'test.tag',
2945+
image: 'https://example.com/image.png',
29032946
click_action: 'test.click.action',
29042947
title_loc_key: 'title.loc.key',
29052948
title_loc_args: ['arg1', 'arg2'],
@@ -3033,6 +3076,7 @@ describe('Messaging', () => {
30333076
},
30343077
fcmOptions: {
30353078
analyticsLabel: 'test.analytics',
3079+
image: 'https://example.com/image.png',
30363080
},
30373081
},
30383082
},
@@ -3069,6 +3113,7 @@ describe('Messaging', () => {
30693113
},
30703114
fcmOptions: {
30713115
analyticsLabel: 'test.analytics',
3116+
image: 'https://example.com/image.png',
30723117
},
30733118
},
30743119
},

0 commit comments

Comments
 (0)