Skip to content

Commit bf5ed9e

Browse files
feat(fcm): Add image in notification support (#115)
Discussion - 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. Resolves #95
1 parent a8cdedb commit bf5ed9e

File tree

5 files changed

+123
-3
lines changed

5 files changed

+123
-3
lines changed

FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public async Task Send()
3838
{
3939
Title = "Title",
4040
Body = "Body",
41+
ImageUrl = "https://example.com/image.png",
4142
},
4243
Android = new AndroidConfig()
4344
{
@@ -61,6 +62,7 @@ public async Task SendAll()
6162
{
6263
Title = "Title",
6364
Body = "Body",
65+
ImageUrl = "https://example.com/image.png",
6466
},
6567
Android = new AndroidConfig()
6668
{

FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public void Notification()
7474
{
7575
Title = "title",
7676
Body = "body",
77+
ImageUrl = "https://example.com/image.png",
7778
},
7879
FcmOptions = new FcmOptions()
7980
{
@@ -88,6 +89,7 @@ public void Notification()
8889
{
8990
{ "title", "title" },
9091
{ "body", "body" },
92+
{ "image", "https://example.com/image.png" },
9193
}
9294
},
9395
{
@@ -219,6 +221,28 @@ public void InvalidTopicNames()
219221
}
220222
}
221223

224+
[Fact]
225+
public void NotificationInvalidImageUrls()
226+
{
227+
var imageUrls = new List<string>()
228+
{
229+
string.Empty, "image.png", "invalid-image", "foo bar",
230+
};
231+
foreach (var imageUrl in imageUrls)
232+
{
233+
var message = new Message()
234+
{
235+
Topic = "test-topic",
236+
Notification = new Notification()
237+
{
238+
ImageUrl = imageUrl,
239+
},
240+
};
241+
242+
Assert.Throws<ArgumentException>(() => message.CopyAndValidate());
243+
}
244+
}
245+
222246
[Fact]
223247
public void AndroidConfig()
224248
{
@@ -244,6 +268,7 @@ public void AndroidConfig()
244268
Color = "#112233",
245269
Sound = "sound",
246270
Tag = "tag",
271+
ImageUrl = "https://example.com/image.png",
247272
ClickAction = "click-action",
248273
TitleLocKey = "title-loc-key",
249274
TitleLocArgs = new List<string>() { "arg1", "arg2" },
@@ -277,6 +302,7 @@ public void AndroidConfig()
277302
{ "color", "#112233" },
278303
{ "sound", "sound" },
279304
{ "tag", "tag" },
305+
{ "image", "https://example.com/image.png" },
280306
{ "click_action", "click-action" },
281307
{ "title_loc_key", "title-loc-key" },
282308
{ "title_loc_args", new JArray() { "arg1", "arg2" } },
@@ -390,6 +416,7 @@ public void AndroidNotificationDeserialization()
390416
Color = "#112233",
391417
Sound = "sound",
392418
Tag = "tag",
419+
ImageUrl = "https://example.com/image.png",
393420
ClickAction = "click-action",
394421
TitleLocKey = "title-loc-key",
395422
TitleLocArgs = new List<string>() { "arg1", "arg2" },
@@ -405,6 +432,7 @@ public void AndroidNotificationDeserialization()
405432
Assert.Equal(original.Color, copy.Color);
406433
Assert.Equal(original.Sound, copy.Sound);
407434
Assert.Equal(original.Tag, copy.Tag);
435+
Assert.Equal(original.ImageUrl, copy.ImageUrl);
408436
Assert.Equal(original.ClickAction, copy.ClickAction);
409437
Assert.Equal(original.TitleLocKey, copy.TitleLocKey);
410438
Assert.Equal(original.TitleLocArgs, copy.TitleLocArgs);
@@ -460,6 +488,30 @@ public void AndroidNotificationInvalidColor()
460488
Assert.Throws<ArgumentException>(() => message.CopyAndValidate());
461489
}
462490

491+
[Fact]
492+
public void AndroidNotificationInvalidImageUrls()
493+
{
494+
var imageUrls = new List<string>()
495+
{
496+
string.Empty, "image.png", "invalid-image", "foo bar",
497+
};
498+
foreach (var imageUrl in imageUrls)
499+
{
500+
var message = new Message()
501+
{
502+
Topic = "test-topic",
503+
Android = new AndroidConfig()
504+
{
505+
Notification = new AndroidNotification()
506+
{
507+
ImageUrl = imageUrl,
508+
},
509+
},
510+
};
511+
Assert.Throws<ArgumentException>(() => message.CopyAndValidate());
512+
}
513+
}
514+
463515
[Fact]
464516
public void AndroidNotificationInvalidTitleLocArgs()
465517
{
@@ -871,6 +923,7 @@ public void ApnsConfig()
871923
FcmOptions = new ApnsFcmOptions()
872924
{
873925
AnalyticsLabel = "label",
926+
ImageUrl = "https://example.com/image.png",
874927
},
875928
},
876929
};
@@ -912,6 +965,7 @@ public void ApnsConfig()
912965
"fcm_options", new JObject()
913966
{
914967
{ "analytics_label", "label" },
968+
{ "image", "https://example.com/image.png" },
915969
}
916970
},
917971
}
@@ -1593,6 +1647,30 @@ public void ApnsInvalidCriticalSoundVolumeTooHigh()
15931647
Assert.Throws<ArgumentException>(() => message.CopyAndValidate());
15941648
}
15951649

1650+
[Fact]
1651+
public void ApnsFcmOptionsInvalidImageUrls()
1652+
{
1653+
var imageUrls = new List<string>()
1654+
{
1655+
string.Empty, "image.png", "invalid-image", "foo bar",
1656+
};
1657+
foreach (var imageUrl in imageUrls)
1658+
{
1659+
var message = new Message()
1660+
{
1661+
Topic = "test-topic",
1662+
Apns = new ApnsConfig()
1663+
{
1664+
FcmOptions = new ApnsFcmOptions()
1665+
{
1666+
ImageUrl = imageUrl,
1667+
},
1668+
},
1669+
};
1670+
Assert.Throws<ArgumentException>(() => message.CopyAndValidate());
1671+
}
1672+
}
1673+
15961674
[Fact]
15971675
public void WebpushNotificationWithLinkUrl()
15981676
{

FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ public sealed class AndroidNotification
6666
[JsonProperty("tag")]
6767
public string Tag { get; set; }
6868

69+
/// <summary>
70+
/// Gets or sets the URL of the image to be displayed in the notification.
71+
/// </summary>
72+
[JsonProperty("image")]
73+
public string ImageUrl { get; set; }
74+
6975
/// <summary>
7076
/// Gets or sets the action associated with a user click on the notification. If specified,
7177
/// an activity with a matching Intent Filter is launched when a user clicks on the
@@ -126,6 +132,7 @@ internal AndroidNotification CopyAndValidate()
126132
Color = this.Color,
127133
Sound = this.Sound,
128134
Tag = this.Tag,
135+
ImageUrl = this.ImageUrl,
129136
ClickAction = this.ClickAction,
130137
TitleLocKey = this.TitleLocKey,
131138
TitleLocArgs = this.TitleLocArgs?.ToList(),
@@ -148,6 +155,11 @@ internal AndroidNotification CopyAndValidate()
148155
throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs.");
149156
}
150157

158+
if (copy.ImageUrl != null && !Uri.IsWellFormedUriString(copy.ImageUrl, UriKind.Absolute))
159+
{
160+
throw new ArgumentException($"Malformed image URL string: {copy.ImageUrl}.");
161+
}
162+
151163
return copy;
152164
}
153165
}

FirebaseAdmin/FirebaseAdmin/Messaging/ApnsFcmOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System;
1516
using FirebaseAdmin.Messaging.Util;
1617
using Newtonsoft.Json;
1718

@@ -28,6 +29,12 @@ public sealed class ApnsFcmOptions
2829
[JsonProperty("analytics_label")]
2930
public string AnalyticsLabel { get; set; }
3031

32+
/// <summary>
33+
/// Gets or sets the URL of the image to be displayed in the notification.
34+
/// </summary>
35+
[JsonProperty("image")]
36+
public string ImageUrl { get; set; }
37+
3138
/// <summary>
3239
/// Copies this FCM options, and validates the content of it to ensure that it can
3340
/// be serialized into the JSON format expected by the FCM service.
@@ -37,9 +44,15 @@ internal ApnsFcmOptions CopyAndValidate()
3744
var copy = new ApnsFcmOptions()
3845
{
3946
AnalyticsLabel = this.AnalyticsLabel,
47+
ImageUrl = this.ImageUrl,
4048
};
4149
AnalyticsLabelChecker.ValidateAnalyticsLabel(copy.AnalyticsLabel);
4250

51+
if (copy.ImageUrl != null && !Uri.IsWellFormedUriString(copy.ImageUrl, UriKind.Absolute))
52+
{
53+
throw new ArgumentException($"Malformed image URL string: {copy.ImageUrl}.");
54+
}
55+
4356
return copy;
4457
}
4558
}

FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System;
1516
using Newtonsoft.Json;
1617

1718
namespace FirebaseAdmin.Messaging
@@ -34,16 +35,30 @@ public sealed class Notification
3435
public string Body { get; set; }
3536

3637
/// <summary>
37-
/// Copies this notification. There is nothing to be validated in this class, but we use
38-
/// the same method name as in other classes in this namespace.
38+
/// Gets or sets the URL of the image to be displayed in the notification.
39+
/// </summary>
40+
[JsonProperty("image")]
41+
public string ImageUrl { get; set; }
42+
43+
/// <summary>
44+
/// Copies this notification, and validates the content of it to ensure that it can
45+
/// be serialized into the JSON format expected by the FCM service.
3946
/// </summary>
4047
internal Notification CopyAndValidate()
4148
{
42-
return new Notification()
49+
var copy = new Notification()
4350
{
4451
Title = this.Title,
4552
Body = this.Body,
53+
ImageUrl = this.ImageUrl,
4654
};
55+
56+
if (copy.ImageUrl != null && !Uri.IsWellFormedUriString(copy.ImageUrl, UriKind.Absolute))
57+
{
58+
throw new ArgumentException($"Malformed image URL string: {copy.ImageUrl}.");
59+
}
60+
61+
return copy;
4762
}
4863
}
4964
}

0 commit comments

Comments
 (0)