From c3b4499b11a1e1c25473d095225579696ce2e817 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 15 Jan 2019 17:25:34 -0800 Subject: [PATCH 1/9] Implementing APNs support --- .../FirebaseAdmin/Messaging/ApnsConfig.cs | 26 +++ FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 180 ++++++++++++++++++ .../FirebaseAdmin/Messaging/ApsAlert.cs | 75 ++++++++ .../FirebaseAdmin/Messaging/CriticalSound.cs | 63 ++++++ 4 files changed, 344 insertions(+) create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs new file mode 100644 index 00000000..acb7ab27 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -0,0 +1,26 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class ApnsConfig + { + public IReadOnlyDictionary Headers; + + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs new file mode 100644 index 00000000..0a0ad144 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -0,0 +1,180 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using Google.Apis.Json; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class Aps + { + [JsonIgnore] + public ApsAlert Alert { get; set; } + + [JsonIgnore] + public string AlertString { get; set; } + + [JsonProperty("alert")] + private object AlertObject + { + get + { + if (Alert != null) + { + return Alert; + } + else if (!string.IsNullOrEmpty(AlertString)) + { + return AlertString; + } + return null; + } + set + { + if (value.GetType() == typeof(string)) + { + AlertString = (string) value; + } + else + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(value); + Alert = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + } + } + + [JsonProperty("badge")] + public int Badge { get; set; } + + [JsonIgnore] + public string Sound { get; set; } + + [JsonIgnore] + public CriticalSound CriticalSound { get; set; } + + [JsonProperty("sound")] + private object SoundObject + { + get + { + if (CriticalSound != null) + { + return CriticalSound; + } + else if (!string.IsNullOrEmpty(Sound)) + { + return Sound; + } + return null; + } + set + { + if (value.GetType() == typeof(string)) + { + Sound = (string) value; + } + else + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(value); + CriticalSound = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + } + } + + [JsonIgnore] + public bool ContentAvailable { get; set; } + + [JsonProperty("content-available")] + private int? ContentAvailableInt + { + get + { + if (ContentAvailable) + { + return 1; + } + return null; + } + set + { + ContentAvailable = (value == 1); + } + } + + [JsonIgnore] + public bool MutableContent { get; set; } + + [JsonProperty("mutable-content")] + private int? MutableContentInt + { + get + { + if (MutableContent) + { + return 1; + } + return null; + } + set + { + MutableContent = (value == 1); + } + } + + [JsonProperty("category")] + public string Category { get; set; } + + [JsonProperty("thread-id")] + public string ThreadId { get; set; } + + /// + /// A collection of arbitrary key-value data to be included in the Aps dictionary. This is + /// exposed as an to support correct + /// deserialization of custom properties. + /// + [JsonExtensionData] + public IDictionary CustomData { get; set; } + + internal Aps CopyAndValidate() + { + var copy = new Aps + { + AlertString = this.AlertString, + Badge = this.Badge, + Sound = this.Sound, + ContentAvailable = this.ContentAvailable, + MutableContent = this.MutableContent, + Category = this.Category, + ThreadId = this.ThreadId, + }; + var apsAlert = this.Alert; + if (apsAlert != null && !string.IsNullOrEmpty(copy.AlertString)) + { + throw new ArgumentException("Multiple specifications for alert (Alert and AlertString"); + } + var criticalSound = this.CriticalSound; + if (criticalSound != null && !string.IsNullOrEmpty(copy.Sound)) + { + throw new ArgumentException("Multiple specifications for sound (CriticalSound and Sound"); + } + + // Copy and validate the child properties + copy.Alert = apsAlert?.CopyAndValidate(); + copy.CriticalSound = criticalSound?.CopyAndValidate(); + return copy; + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs new file mode 100644 index 00000000..1a863156 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -0,0 +1,75 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class ApsAlert + { + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("subtitle")] + public string Subtitle { get; set; } + + [JsonProperty("body")] + public string Body { get; set; } + + [JsonProperty("loc-key")] + public string LocKey { get; set; } + + [JsonProperty("loc-args")] + public IEnumerable LocArgs { get; set; } + + [JsonProperty("title-loc-key")] + public string TitleLocKey { get; set; } + + [JsonProperty("title-loc-args")] + public IEnumerable TitleLocArgs { get; set; } + + [JsonProperty("subtitle-loc-key")] + public string SubtitleLocKey { get; set; } + + [JsonProperty("subtitle-loc-args")] + public IEnumerable SubtitleLocArgs { get; set; } + + [JsonProperty("action-loc-key")] + public string ActionLocKey { get; set; } + + [JsonProperty("launch-image")] + public string LaunchImage { get; set; } + + internal ApsAlert CopyAndValidate() + { + return new ApsAlert() + { + Title = this.Title, + Subtitle = this.Subtitle, + Body = this.Body, + LocKey = this.LocKey, + LocArgs = this.LocArgs?.ToList(), + TitleLocKey = this.TitleLocKey, + TitleLocArgs = this.TitleLocArgs?.ToList(), + SubtitleLocKey = this.SubtitleLocKey, + SubtitleLocArgs = this.SubtitleLocArgs?.ToList(), + ActionLocKey = this.ActionLocKey, + LaunchImage = this.LaunchImage, + }; + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs new file mode 100644 index 00000000..fdf319ef --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -0,0 +1,63 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class CriticalSound + { + [JsonIgnore] + public bool Critical { get; set; } + + [JsonProperty("critical")] + private int? CriticalInt + { + get + { + if (Critical) + { + return 1; + } + return null; + } + set + { + Critical = (value == 1); + } + } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("volume")] + public double Volume { get; set; } + + internal CriticalSound CopyAndValidate() + { + if (Volume < 0 || Volume > 1) + { + throw new ArgumentException("Volume must be in the interval [0, 1]"); + } + return new CriticalSound() + { + Critical = this.Critical, + Name = this.Name, + Volume = this.Volume, + } + } + } +} \ No newline at end of file From 693eac5a7f9a8f7c353a4614b8b848b40ff96c7c Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 16 Jan 2019 17:41:55 -0800 Subject: [PATCH 2/9] Added other APNS types --- .../Messaging/MessageTest.cs | 75 ++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 88 +++++++++++ FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 137 ++++++++++++++---- .../FirebaseAdmin/Messaging/ApsAlert.cs | 51 +++++++ .../FirebaseAdmin/Messaging/CriticalSound.cs | 25 +++- .../FirebaseAdmin/Messaging/Message.cs | 7 + 6 files changed, 354 insertions(+), 29 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 0fd03535..b2f58b10 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -799,6 +799,81 @@ public void WebpushNotificationDuplicateKeys() Assert.Throws(() => message.CopyAndValidate()); } + [Fact] + public void ApnsConfig() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Headers = new Dictionary() + { + {"k1", "v1"}, + {"k2", "v2"}, + }, + Aps = new Aps() + { + AlertString = "alert-text", + Badge = 0, + Category = "test-category", + ContentAvailable = true, + MutableContent = true, + Sound = "sound-file", + ThreadId = "test-thread", + CustomData = new Dictionary() + { + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + }, + }, + CustomData = new Dictionary() + { + {"custom-key3", "custom-data"}, + {"custom-key4", true}, + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "headers", new JObject() + { + {"k1", "v1"}, + {"k2", "v2"}, + } + }, + { + "payload", new JObject() + { + { + "aps", new JObject() + { + {"alert", "alert-text"}, + {"badge", 0}, + {"category", "test-category"}, + {"content-available", 1}, + {"mutable-content", 1}, + {"sound", "sound-file"}, + {"thread-id", "test-thread"}, + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + } + }, + {"custom-key3", "custom-data"}, + {"custom-key4", true}, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index acb7ab27..8211d4ec 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -14,13 +14,101 @@ using System; using System.Collections.Generic; +using System.Linq; +using Google.Apis.Json; using Newtonsoft.Json; namespace FirebaseAdmin.Messaging { + /// + /// Represents the APNS-specific options that can be included in a . Refer + /// to + /// APNs documentation for various headers and payload fields supported by APNS. + /// public sealed class ApnsConfig { + /// + /// A collection of APNs headers. + /// + [JsonProperty("headers")] public IReadOnlyDictionary Headers; + /// + /// The aps dictionary to be included in the APNs payload. + /// + [JsonIgnore] + public Aps Aps; + + /// + /// APNs payload as accepted by the FCM backend servers. + /// + [JsonProperty("payload")] + private IReadOnlyDictionary Payload + { + get + { + var aps = this.Aps; + if (aps == null) + { + throw new ArgumentException("Aps must not be null in ApnsConfig."); + } + var payload = new Dictionary() + { + { "aps", aps }, + }; + var customData = this.CustomData; + if (customData != null) + { + if (customData.ContainsKey("aps")) + { + throw new ArgumentException( + "Multiple specifications for Apns payload key: aps"); + } + payload = payload.Concat(customData).ToDictionary(x=>x.Key, x=>x.Value); + } + return payload; + } + set + { + var copy = value.ToDictionary(x=>x.Key, x=>x.Value); + object aps; + if (copy.TryGetValue("aps", out aps)) + { + copy.Remove("aps"); + if (aps.GetType() == typeof(Aps)) + { + this.Aps = (Aps) aps; + } + else + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(aps); + this.Aps = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + } + this.CustomData = copy; + } + } + + /// + /// A collection of arbitrary key-value data to be included in the APNs payload. + /// + [JsonIgnore] + public IReadOnlyDictionary CustomData { get; set; } + + /// + /// Copies this APNs config, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. + /// + + internal ApnsConfig CopyAndValidate() + { + var copy = new ApnsConfig() + { + Headers = this.Headers.Copy(), + Payload = this.Payload, + }; + copy.Aps = copy.Aps?.CopyAndValidate(); + return copy; + } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index 0a0ad144..add0cb5c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -19,11 +19,24 @@ namespace FirebaseAdmin.Messaging { + /// + /// Represents the + /// aps dictionary that is part of every APNs message. + /// public sealed class Aps { + /// + /// An advanced alert configuration to be included in the message. It is an error to set + /// both and properties together. + /// [JsonIgnore] public ApsAlert Alert { get; set; } + /// + /// The alert text to be included in the message. To specify a more advanced alert + /// configuration, use the property instead. It is an error to set + /// both and properties together. + /// [JsonIgnore] public string AlertString { get; set; } @@ -32,22 +45,32 @@ private object AlertObject { get { - if (Alert != null) + object alert = this.AlertString; + if (string.IsNullOrEmpty(alert as string)) { - return Alert; + alert = this.Alert; } - else if (!string.IsNullOrEmpty(AlertString)) + else if (this.Alert != null) { - return AlertString; + throw new ArgumentException( + "Multiple specifications for alert (Alert and AlertString"); } - return null; + return alert; } set { - if (value.GetType() == typeof(string)) + if (value == null) + { + return; + } + else if (value.GetType() == typeof(string)) { AlertString = (string) value; } + else if (value.GetType() == typeof(ApsAlert)) + { + Alert = (ApsAlert) value; + } else { var json = NewtonsoftJsonSerializer.Instance.Serialize(value); @@ -56,12 +79,26 @@ private object AlertObject } } + /// + /// The badge to be displayed with the message. Set to 0 to remove the badge. When not + /// specified, the badge will remain unchanged. + /// [JsonProperty("badge")] - public int Badge { get; set; } + public int? Badge { get; set; } + /// + /// The name of a sound file in your app's main bundle or in the + /// Library/Sounds folder of your app's container directory. Specify the + /// string default to play the system sound. It is an error to set both + /// and properties together. + /// [JsonIgnore] public string Sound { get; set; } + /// + /// The critical alert sound to be played with the message. It is an error to set both + /// and properties together. + /// [JsonIgnore] public CriticalSound CriticalSound { get; set; } @@ -70,22 +107,32 @@ private object SoundObject { get { - if (CriticalSound != null) + object sound = this.Sound; + if (string.IsNullOrEmpty(sound as string)) { - return CriticalSound; + sound = this.CriticalSound; } - else if (!string.IsNullOrEmpty(Sound)) + else if (this.CriticalSound != null) { - return Sound; + throw new ArgumentException( + "Multiple specifications for sound (CriticalSound and Sound"); } - return null; + return sound; } set { - if (value.GetType() == typeof(string)) + if (value == null) + { + return; + } + else if (value.GetType() == typeof(string)) { Sound = (string) value; } + else if (value.GetType() == typeof(CriticalSound)) + { + CriticalSound = (CriticalSound) value; + } else { var json = NewtonsoftJsonSerializer.Instance.Serialize(value); @@ -94,9 +141,16 @@ private object SoundObject } } + /// + /// Specifies whether to configure a background update notification. + /// [JsonIgnore] public bool ContentAvailable { get; set; } + /// + /// Integer representation of the property, which is how + /// APNs expects it. + /// [JsonProperty("content-available")] private int? ContentAvailableInt { @@ -114,9 +168,17 @@ private int? ContentAvailableInt } } + /// + /// Specifies whether to set the mutable-content property on the message. When + /// set, this property allows clients to modify the notification via app extensions. + /// [JsonIgnore] public bool MutableContent { get; set; } + /// + /// Integer representation of the property, which is how + /// APNs expects it. + /// [JsonProperty("mutable-content")] private int? MutableContentInt { @@ -134,46 +196,65 @@ private int? MutableContentInt } } + /// + /// The type of the notification. + /// [JsonProperty("category")] public string Category { get; set; } + /// + /// An app-specific identifier for grouping notifications. + /// [JsonProperty("thread-id")] public string ThreadId { get; set; } /// - /// A collection of arbitrary key-value data to be included in the Aps dictionary. This is - /// exposed as an to support correct - /// deserialization of custom properties. + /// A collection of arbitrary key-value data to be included in the aps + /// dictionary. This is exposed as an to support + /// correct deserialization of custom properties. /// [JsonExtensionData] public IDictionary CustomData { get; set; } + /// + /// Copies this Aps dictionary, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM and APNs services. + /// internal Aps CopyAndValidate() { var copy = new Aps { - AlertString = this.AlertString, + AlertObject = this.AlertObject, Badge = this.Badge, - Sound = this.Sound, + SoundObject = this.SoundObject, ContentAvailable = this.ContentAvailable, MutableContent = this.MutableContent, Category = this.Category, ThreadId = this.ThreadId, }; - var apsAlert = this.Alert; - if (apsAlert != null && !string.IsNullOrEmpty(copy.AlertString)) - { - throw new ArgumentException("Multiple specifications for alert (Alert and AlertString"); - } - var criticalSound = this.CriticalSound; - if (criticalSound != null && !string.IsNullOrEmpty(copy.Sound)) + + var customData = this.CustomData; + if (customData?.Count > 0) { - throw new ArgumentException("Multiple specifications for sound (CriticalSound and Sound"); + var serializer = NewtonsoftJsonSerializer.Instance; + // Serialize the notification without CustomData for validation. + var json = serializer.Serialize(copy); + var dict = serializer.Deserialize>(json); + customData = new Dictionary(customData); + foreach (var entry in customData) + { + if (dict.ContainsKey(entry.Key)) + { + throw new ArgumentException( + $"Multiple specifications for Aps key: {entry.Key}"); + } + } + copy.CustomData = customData; } // Copy and validate the child properties - copy.Alert = apsAlert?.CopyAndValidate(); - copy.CriticalSound = criticalSound?.CopyAndValidate(); + copy.Alert = copy.Alert?.CopyAndValidate(); + copy.CriticalSound = copy.CriticalSound?.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs index 1a863156..9394eb3b 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -19,41 +19,92 @@ namespace FirebaseAdmin.Messaging { + /// + /// Represents the + /// alert property that can be included in the aps dictionary of an APNs + /// payload. + /// public sealed class ApsAlert { + /// + /// The title of the alert. When provided, overrides the title set via + /// . + /// [JsonProperty("title")] public string Title { get; set; } + /// + /// The subtitle of the alert. + /// [JsonProperty("subtitle")] public string Subtitle { get; set; } + /// + /// The body of the alert. When provided, overrides the body set via + /// . + /// [JsonProperty("body")] public string Body { get; set; } + /// + /// The key of the body string in the app's string resources to use to localize the body + /// text. + /// [JsonProperty("loc-key")] public string LocKey { get; set; } + /// + /// Resource key strings that will be used in place of the format specifiers in + /// . + /// [JsonProperty("loc-args")] public IEnumerable LocArgs { get; set; } + /// + /// The key of the title string in the app's string resources to use to localize the title + /// text. + /// [JsonProperty("title-loc-key")] public string TitleLocKey { get; set; } + /// + /// Resource key strings that will be used in place of the format specifiers in + /// . + /// [JsonProperty("title-loc-args")] public IEnumerable TitleLocArgs { get; set; } + /// + /// The key of the subtitle string in the app's string resources to use to localize the + /// subtitle text. + /// [JsonProperty("subtitle-loc-key")] public string SubtitleLocKey { get; set; } + /// + /// Resource key strings that will be used in place of the format specifiers in + /// . + /// [JsonProperty("subtitle-loc-args")] public IEnumerable SubtitleLocArgs { get; set; } + /// + /// The key of the text in the app's string resources to use to localize the action button + /// text. + /// [JsonProperty("action-loc-key")] public string ActionLocKey { get; set; } + /// + /// The launch image for the notification action. + /// [JsonProperty("launch-image")] public string LaunchImage { get; set; } + /// + /// Copies this alert dictionary, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM and APNs services. + /// internal ApsAlert CopyAndValidate() { return new ApsAlert() diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index fdf319ef..2c689ca4 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -18,11 +18,21 @@ namespace FirebaseAdmin.Messaging { + /// + /// The sound configuration for APNs critical alerts. + /// public sealed class CriticalSound { + /// + /// Sets the critical alert flag on the sound configuration. + /// [JsonIgnore] public bool Critical { get; set; } + /// + /// Integer representation of the property, which is how + /// APNs expects it. + /// [JsonProperty("critical")] private int? CriticalInt { @@ -40,12 +50,25 @@ private int? CriticalInt } } + /// + /// The name of a sound file in your app's main bundle or in the + /// Library/Sounds folder of your app's container directory. Specify the + /// string default to play the system sound. + /// [JsonProperty("name")] public string Name { get; set; } + /// + /// The volume for the critical alert's sound. Must be a value between 0.0 (silent) and + /// 1.0 (full volume). + /// [JsonProperty("volume")] public double Volume { get; set; } + /// + /// Copies this critical sound configuration, and validates the content of it to ensure + /// that it can be serialized into the JSON format expected by the FCM and APNs services. + /// internal CriticalSound CopyAndValidate() { if (Volume < 0 || Volume > 1) @@ -57,7 +80,7 @@ internal CriticalSound CopyAndValidate() Critical = this.Critical, Name = this.Name, Volume = this.Volume, - } + }; } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 6635761e..64c226c8 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -95,6 +95,12 @@ private string UnprefixedTopic [JsonProperty("webpush")] public WebpushConfig Webpush { get; set; } + /// + /// The APNs-specific information to be included in the message. + /// + [JsonProperty("apns")] + public ApnsConfig Apns { get; set; } + /// /// Copies this message, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. Each property is copied @@ -131,6 +137,7 @@ internal Message CopyAndValidate() copy.Notification = this.Notification?.CopyAndValidate(); copy.Android = this.Android?.CopyAndValidate(); copy.Webpush = this.Webpush?.CopyAndValidate(); + copy.Apns = this.Apns?.CopyAndValidate(); return copy; } } From a4edaf142b8929bd0b1394da40a6a43d759e7db8 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 16 Jan 2019 22:28:42 -0800 Subject: [PATCH 3/9] Cleaning up the Apns impl --- .../Messaging/MessageTest.cs | 59 +++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 37 +++++++----- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 31 +++++----- .../FirebaseAdmin/Messaging/CriticalSound.cs | 5 +- 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index b2f58b10..d6bd3ddd 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -874,6 +874,65 @@ public void ApnsConfig() AssertJsonEquals(expected, message); } + [Fact] + public void ApnsNoAps() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + CustomData = new Dictionary() + { + {"test", "custom-data"}, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void ApnsDuplicateAps() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + AlertString = "alert-text", + }, + CustomData = new Dictionary() + { + {"aps", "custom-data"}, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void ApnsDuplicateApsAlerts() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + AlertString = "alert-text", + Alert = new ApsAlert() + { + Body = "other-alert-text", + }, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 8211d4ec..6bcd32c9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -47,24 +47,17 @@ private IReadOnlyDictionary Payload { get { + var payload = this.CustomData?.ToDictionary(e => e.Key, e => e.Value) + ?? new Dictionary(); var aps = this.Aps; - if (aps == null) + if (aps != null) { - throw new ArgumentException("Aps must not be null in ApnsConfig."); - } - var payload = new Dictionary() - { - { "aps", aps }, - }; - var customData = this.CustomData; - if (customData != null) - { - if (customData.ContainsKey("aps")) + if (payload.ContainsKey("aps")) { throw new ArgumentException( "Multiple specifications for Apns payload key: aps"); } - payload = payload.Concat(customData).ToDictionary(x=>x.Key, x=>x.Value); + payload["aps"] = aps; } return payload; } @@ -104,10 +97,24 @@ internal ApnsConfig CopyAndValidate() { var copy = new ApnsConfig() { - Headers = this.Headers.Copy(), - Payload = this.Payload, + Headers = this.Headers?.Copy(), + CustomData = this.CustomData?.Copy(), }; - copy.Aps = copy.Aps?.CopyAndValidate(); + var aps = this.Aps; + if (copy.CustomData != null && copy.CustomData.ContainsKey("aps")) + { + if (aps != null) + { + throw new ArgumentException( + "Multiple specifications for Apns payload key: aps"); + } + } + else if (aps == null) + { + throw new ArgumentException( + "Aps dictionary is required in ApnsConfig"); + } + copy.Aps = aps?.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index add0cb5c..335e358d 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Google.Apis.Json; using Newtonsoft.Json; @@ -25,6 +26,8 @@ namespace FirebaseAdmin.Messaging /// public sealed class Aps { + private static readonly NewtonsoftJsonSerializer Serializer = NewtonsoftJsonSerializer.Instance; + /// /// An advanced alert configuration to be included in the message. It is an error to set /// both and properties together. @@ -73,8 +76,8 @@ private object AlertObject } else { - var json = NewtonsoftJsonSerializer.Instance.Serialize(value); - Alert = NewtonsoftJsonSerializer.Instance.Deserialize(json); + var json = Serializer.Serialize(value); + Alert = Serializer.Deserialize(json); } } } @@ -135,8 +138,8 @@ private object SoundObject } else { - var json = NewtonsoftJsonSerializer.Instance.Serialize(value); - CriticalSound = NewtonsoftJsonSerializer.Instance.Deserialize(json); + var json = Serializer.Serialize(value); + CriticalSound = Serializer.Deserialize(json); } } } @@ -226,33 +229,31 @@ internal Aps CopyAndValidate() { AlertObject = this.AlertObject, Badge = this.Badge, - SoundObject = this.SoundObject, ContentAvailable = this.ContentAvailable, MutableContent = this.MutableContent, Category = this.Category, + SoundObject = this.SoundObject, ThreadId = this.ThreadId, }; - var customData = this.CustomData; + var customData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value); if (customData?.Count > 0) { var serializer = NewtonsoftJsonSerializer.Instance; // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); - var dict = serializer.Deserialize>(json); - customData = new Dictionary(customData); - foreach (var entry in customData) + var standardApsProperties = serializer.Deserialize>(json); + var duplicates = customData.Keys + .Where(customKey => standardApsProperties.ContainsKey(customKey)) + .ToList(); + if (duplicates.Any()) { - if (dict.ContainsKey(entry.Key)) - { - throw new ArgumentException( - $"Multiple specifications for Aps key: {entry.Key}"); - } + throw new ArgumentException( + $"Multiple specifications for Aps keys: {string.Join(",", duplicates)}"); } copy.CustomData = customData; } - // Copy and validate the child properties copy.Alert = copy.Alert?.CopyAndValidate(); copy.CriticalSound = copy.CriticalSound?.CopyAndValidate(); return copy; diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index 2c689ca4..f4bfac67 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -71,7 +71,8 @@ private int? CriticalInt /// internal CriticalSound CopyAndValidate() { - if (Volume < 0 || Volume > 1) + var volume = this.Volume; + if (volume < 0 || volume > 1) { throw new ArgumentException("Volume must be in the interval [0, 1]"); } @@ -79,7 +80,7 @@ internal CriticalSound CopyAndValidate() { Critical = this.Critical, Name = this.Name, - Volume = this.Volume, + Volume = volume, }; } } From 31e317b5e14c581c7742174fbca7467af71b8977 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 16 Jan 2019 23:51:23 -0800 Subject: [PATCH 4/9] More clean up and tests --- .../Messaging/MessageTest.cs | 288 ++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 19 +- .../FirebaseAdmin/Messaging/CriticalSound.cs | 20 +- 3 files changed, 304 insertions(+), 23 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index d6bd3ddd..cccd2ca4 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -874,6 +874,214 @@ public void ApnsConfig() AssertJsonEquals(expected, message); } + [Fact] + public void ApnsConfigMinimal() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps(), + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "payload", new JObject() + { + {"aps", new JObject()}, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void ApnsCriticalSound() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + CriticalSound = new CriticalSound() + { + Name = "default", + Critical = true, + Volume = 0.5, + }, + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "payload", new JObject() + { + { + "aps", new JObject() + { + { + "sound", new JObject() + { + {"name", "default"}, + {"critical", 1}, + {"volume", 0.5}, + } + }, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void ApnsCriticalSoundMinimal() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + CriticalSound = new CriticalSound(){Name = "default"}, + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "payload", new JObject() + { + { + "aps", new JObject() + { + { + "sound", new JObject() + { + {"name", "default"}, + } + }, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void ApnsCustomApsWithStandardProperties() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + CustomData = new Dictionary() + { + { + "aps", new Dictionary() + { + {"alert", "alert-text"}, + {"badge", 42}, + } + }, + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "payload", new JObject() + { + { + "aps", new JObject() + { + {"alert", "alert-text"}, + {"badge", 42}, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void ApnsCustomApsWithCustomProperties() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + CustomData = new Dictionary() + { + { + "aps", new Dictionary() + { + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + } + }, + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "payload", new JObject() + { + { + "aps", new JObject() + { + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + [Fact] public void ApnsNoAps() { @@ -933,6 +1141,86 @@ public void ApnsDuplicateApsAlerts() Assert.Throws(() => message.CopyAndValidate()); } + [Fact] + public void ApnsDuplicateSounds() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + Sound = "default", + CriticalSound = new CriticalSound() + { + Name = "other=sound", + }, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void ApnsInvalidCriticalSoundNoName() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + CriticalSound = new CriticalSound(), + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void ApnsInvalidCriticalSoundVolumeTooLow() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + CriticalSound = new CriticalSound() + { + Name = "default", + Volume = -0.1, + }, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void ApnsInvalidCriticalSoundVolumeTooHigh() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + CriticalSound = new CriticalSound() + { + Name = "default", + Volume = 1.1, + }, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 6bcd32c9..5fa881bd 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -92,29 +92,18 @@ private IReadOnlyDictionary Payload /// Copies this APNs config, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. /// - internal ApnsConfig CopyAndValidate() { var copy = new ApnsConfig() { Headers = this.Headers?.Copy(), - CustomData = this.CustomData?.Copy(), + Payload = this.Payload, }; - var aps = this.Aps; - if (copy.CustomData != null && copy.CustomData.ContainsKey("aps")) - { - if (aps != null) - { - throw new ArgumentException( - "Multiple specifications for Apns payload key: aps"); - } - } - else if (aps == null) + if (copy.Aps == null) { - throw new ArgumentException( - "Aps dictionary is required in ApnsConfig"); + throw new ArgumentException("Aps dictionary is required in ApnsConfig"); } - copy.Aps = aps?.CopyAndValidate(); + copy.Aps = copy.Aps.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index f4bfac67..636fe5b7 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -63,7 +63,7 @@ private int? CriticalInt /// 1.0 (full volume). /// [JsonProperty("volume")] - public double Volume { get; set; } + public double? Volume { get; set; } /// /// Copies this critical sound configuration, and validates the content of it to ensure @@ -71,17 +71,21 @@ private int? CriticalInt /// internal CriticalSound CopyAndValidate() { - var volume = this.Volume; - if (volume < 0 || volume > 1) - { - throw new ArgumentException("Volume must be in the interval [0, 1]"); - } - return new CriticalSound() + var copy = new CriticalSound() { Critical = this.Critical, Name = this.Name, - Volume = volume, + Volume = this.Volume, }; + if (string.IsNullOrEmpty(copy.Name)) + { + throw new ArgumentException("Name must be specified for CriticalSound"); + } + if (copy.Volume < 0 || copy.Volume > 1) + { + throw new ArgumentException("Volume of CriticalSound must be in the interval [0, 1]"); + } + return copy; } } } \ No newline at end of file From 0eade9c1785ff04281e954642ee9db370243a0c3 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 17 Jan 2019 01:16:05 -0800 Subject: [PATCH 5/9] Using more Linq; More tests --- .../Messaging/MessageTest.cs | 16 ++++++++++++- .../Messaging/AndroidNotification.cs | 16 ++++--------- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 17 ++++--------- .../Messaging/WebpushNotification.cs | 24 +++++++++---------- 4 files changed, 36 insertions(+), 37 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index cccd2ca4..3de2c00c 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -1120,6 +1120,20 @@ public void ApnsDuplicateAps() Assert.Throws(() => message.CopyAndValidate()); } + [Fact] + public void ApsDuplicateKeys() + { + var aps = new Aps() + { + AlertString = "alert-text", + CustomData = new Dictionary() + { + {"alert", "other-alert-text"}, + }, + }; + Assert.Throws(() => aps.CopyAndValidate()); + } + [Fact] public void ApnsDuplicateApsAlerts() { @@ -1142,7 +1156,7 @@ public void ApnsDuplicateApsAlerts() } [Fact] - public void ApnsDuplicateSounds() + public void ApnsDuplicateApsSounds() { var message = new Message() { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 417efe70..24e546fa 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -135,21 +135,15 @@ internal AndroidNotification CopyAndValidate() }; if (copy.Color != null && !Regex.Match(copy.Color, "^#[0-9a-fA-F]{6}$").Success) { - throw new ArgumentException("Color must be in the form #RRGGBB"); + throw new ArgumentException("Color must be in the form #RRGGBB."); } - if (copy.TitleLocArgs != null && copy.TitleLocArgs.Any()) + if (copy.TitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.TitleLocKey)) { - if (string.IsNullOrEmpty(copy.TitleLocKey)) - { - throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs"); - } + throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs."); } - if (copy.BodyLocArgs != null && copy.BodyLocArgs.Any()) + if (copy.BodyLocArgs?.Any() == true && string.IsNullOrEmpty(copy.BodyLocKey)) { - if (string.IsNullOrEmpty(copy.BodyLocKey)) - { - throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs"); - } + throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs."); } return copy; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index 335e358d..a8bbd4c2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -159,11 +159,7 @@ private int? ContentAvailableInt { get { - if (ContentAvailable) - { - return 1; - } - return null; + return ContentAvailable ? 1 : (int?) null; } set { @@ -187,11 +183,7 @@ private int? MutableContentInt { get { - if (MutableContent) - { - return 1; - } - return null; + return MutableContent ? 1 : (int?) null; } set { @@ -240,11 +232,10 @@ internal Aps CopyAndValidate() if (customData?.Count > 0) { var serializer = NewtonsoftJsonSerializer.Instance; - // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); - var standardApsProperties = serializer.Deserialize>(json); + var standardProperties = serializer.Deserialize>(json); var duplicates = customData.Keys - .Where(customKey => standardApsProperties.ContainsKey(customKey)) + .Where(customProperty => standardProperties.ContainsKey(customProperty)) .ToList(); if (duplicates.Any()) { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 10dd7fb3..602d5c01 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -100,8 +100,9 @@ private string DirectionString Direction = Messaging.Direction.RightToLeft; return; default: - throw new FirebaseException($"Invalid direction value: {value}. Only " - + "'auto', 'rtl' and 'ltr' are allowed."); + throw new FirebaseException( + $"Invalid direction value: {value}. Only 'auto', 'rtl' and 'ltr' " + + "are allowed."); } } } @@ -195,21 +196,20 @@ internal WebpushNotification CopyAndValidate() Data = this.Data, }; - var customData = this.CustomData; + var customData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value); if (customData?.Count > 0) { var serializer = NewtonsoftJsonSerializer.Instance; - // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); - var dict = serializer.Deserialize>(json); - customData = new Dictionary(customData); - foreach (var entry in customData) + var standardProperties = serializer.Deserialize>(json); + var duplicates = customData.Keys + .Where(customProperty => standardProperties.ContainsKey(customProperty)) + .ToList(); + if (duplicates.Any()) { - if (dict.ContainsKey(entry.Key)) - { - throw new ArgumentException( - $"Multiple specifications for WebpushNotification key: {entry.Key}"); - } + throw new ArgumentException( + "Multiple specifications for WebpushNotification keys: " + + string.Join(",", duplicates)); } copy.CustomData = customData; } From 6124d4590eea439d09e406c62a6d1023573b4fc3 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 17 Jan 2019 18:17:56 -0800 Subject: [PATCH 6/9] Fixed Apns deserialization --- .../Messaging/MessageTest.cs | 244 ++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 86 +++--- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 2 +- .../FirebaseAdmin/Messaging/ApsAlert.cs | 15 +- .../Messaging/WebpushNotification.cs | 2 +- 5 files changed, 312 insertions(+), 37 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 3de2c00c..e7b1837a 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -903,6 +903,63 @@ public void ApnsConfigMinimal() AssertJsonEquals(expected, message); } + [Fact] + public void ApnsConfigDeserialization() + { + var original = new ApnsConfig() + { + Headers = new Dictionary() + { + {"k1", "v1"}, + {"k2", "v2"}, + }, + Aps = new Aps() + { + AlertString = "alert-text", + }, + CustomData = new Dictionary() + { + {"custom-key3", "custom-data"}, + {"custom-key4", true}, + }, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Headers, copy.Headers); + Assert.Equal(original.CustomData, copy.CustomData); + Assert.Equal(original.Aps.AlertString, copy.Aps.AlertString); + } + + [Fact] + public void ApnsConfigCustomApsDeserialization() + { + var original = new ApnsConfig() + { + Headers = new Dictionary() + { + {"k1", "v1"}, + {"k2", "v2"}, + }, + CustomData = new Dictionary() + { + { + "aps", new Dictionary() + { + {"alert", "alert-text"}, + } + }, + {"custom-key3", "custom-data"}, + {"custom-key4", true}, + }, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Headers, copy.Headers); + original.CustomData.Remove("aps"); + Assert.Equal(original.CustomData, copy.CustomData); + Assert.Equal("alert-text", copy.Aps.AlertString); + } + [Fact] public void ApnsCriticalSound() { @@ -994,6 +1051,170 @@ public void ApnsCriticalSoundMinimal() AssertJsonEquals(expected, message); } + [Fact] + public void ApnsApsAlert() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + Alert = new ApsAlert() + { + ActionLocKey = "action-key", + Body = "test-body", + LaunchImage = "test-image", + LocArgs = new List(){"arg1", "arg2"}, + LocKey = "loc-key", + Subtitle = "test-subtitle", + SubtitleLocArgs = new List(){"arg3", "arg4"}, + SubtitleLocKey = "subtitle-key", + Title = "test-title", + TitleLocArgs = new List(){"arg5", "arg6"}, + TitleLocKey = "title-key", + }, + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "payload", new JObject() + { + { + "aps", new JObject() + { + { + "alert", new JObject() + { + {"action-loc-key", "action-key"}, + {"body", "test-body"}, + {"launch-image", "test-image"}, + {"loc-args", new JArray(){"arg1", "arg2"}}, + {"loc-key", "loc-key"}, + {"subtitle", "test-subtitle"}, + {"subtitle-loc-args", new JArray(){"arg3", "arg4"}}, + {"subtitle-loc-key", "subtitle-key"}, + {"title", "test-title"}, + {"title-loc-args", new JArray(){"arg5", "arg6"}}, + {"title-loc-key", "title-key"}, + } + }, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void ApnsApsAlertMinimal() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + Alert = new ApsAlert(), + }, + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "apns", new JObject() + { + { + "payload", new JObject() + { + { + "aps", new JObject() + { + { + "alert", new JObject(){} + }, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void ApnsApsAlertInvalidTitleLocArgs() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + Alert = new ApsAlert() + { + TitleLocArgs = new List(){"arg1", "arg2"}, + }, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void ApnsApsAlertInvalidSubtitleLocArgs() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + Alert = new ApsAlert() + { + SubtitleLocArgs = new List(){"arg1", "arg2"}, + }, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void ApnsApsAlertInvalidLocArgs() + { + var message = new Message() + { + Topic = "test-topic", + Apns = new ApnsConfig() + { + Aps = new Aps() + { + Alert = new ApsAlert() + { + LocArgs = new List(){"arg1", "arg2"}, + }, + }, + }, + }; + Assert.Throws(() => message.CopyAndValidate()); + } + [Fact] public void ApnsCustomApsWithStandardProperties() { @@ -1244,4 +1465,27 @@ private void AssertJsonEquals(JObject expected, Message actual) $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } + + internal class Hello + { + + [JsonIgnore] + internal Aps Aps { get; set; } + + [JsonProperty("payload")] + internal ApnsPayload Payload + { + get + { + return new ApnsPayload() + { + Aps = this.Aps, + }; + } + set + { + this.Aps = value.Aps; + } + } + } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 5fa881bd..a9b4efb9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -27,58 +27,47 @@ namespace FirebaseAdmin.Messaging /// public sealed class ApnsConfig { + private ApnsPayload _payload = new ApnsPayload(); + /// /// A collection of APNs headers. /// [JsonProperty("headers")] - public IReadOnlyDictionary Headers; + public IReadOnlyDictionary Headers { get; set; } /// /// The aps dictionary to be included in the APNs payload. /// [JsonIgnore] - public Aps Aps; + public Aps Aps + { + get + { + return Payload.Aps; + } + set + { + Payload.Aps = value; + } + } /// /// APNs payload as accepted by the FCM backend servers. /// [JsonProperty("payload")] - private IReadOnlyDictionary Payload + private ApnsPayload Payload { get { - var payload = this.CustomData?.ToDictionary(e => e.Key, e => e.Value) - ?? new Dictionary(); - var aps = this.Aps; - if (aps != null) + if (_payload.Aps != null && _payload.CustomData?.ContainsKey("aps") == true) { - if (payload.ContainsKey("aps")) - { - throw new ArgumentException( - "Multiple specifications for Apns payload key: aps"); - } - payload["aps"] = aps; + throw new ArgumentException("Multiple specifications for ApnsConfig key: aps"); } - return payload; + return _payload; } set { - var copy = value.ToDictionary(x=>x.Key, x=>x.Value); - object aps; - if (copy.TryGetValue("aps", out aps)) - { - copy.Remove("aps"); - if (aps.GetType() == typeof(Aps)) - { - this.Aps = (Aps) aps; - } - else - { - var json = NewtonsoftJsonSerializer.Instance.Serialize(aps); - this.Aps = NewtonsoftJsonSerializer.Instance.Deserialize(json); - } - } - this.CustomData = copy; + _payload = value; } } @@ -86,7 +75,17 @@ private IReadOnlyDictionary Payload /// A collection of arbitrary key-value data to be included in the APNs payload. /// [JsonIgnore] - public IReadOnlyDictionary CustomData { get; set; } + public IDictionary CustomData + { + get + { + return Payload.CustomData; + } + set + { + Payload.CustomData = value; + } + } /// /// Copies this APNs config, and validates the content of it to ensure that it can be @@ -97,13 +96,32 @@ internal ApnsConfig CopyAndValidate() var copy = new ApnsConfig() { Headers = this.Headers?.Copy(), - Payload = this.Payload, + Payload = this.Payload.CopyAndValidate(), + }; + return copy; + } + } + + internal class ApnsPayload + { + [JsonProperty("aps")] + public Aps Aps { get; set; } + + [JsonExtensionData] + public IDictionary CustomData { get; set; } + + internal ApnsPayload CopyAndValidate() + { + var copy = new ApnsPayload() + { + CustomData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value), }; - if (copy.Aps == null) + var aps = this.Aps; + if (aps == null && copy.CustomData?.ContainsKey("aps") == false) { throw new ArgumentException("Aps dictionary is required in ApnsConfig"); } - copy.Aps = copy.Aps.CopyAndValidate(); + copy.Aps = aps?.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index a8bbd4c2..585f4452 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -235,7 +235,7 @@ internal Aps CopyAndValidate() var json = serializer.Serialize(copy); var standardProperties = serializer.Deserialize>(json); var duplicates = customData.Keys - .Where(customProperty => standardProperties.ContainsKey(customProperty)) + .Where(customKey => standardProperties.ContainsKey(customKey)) .ToList(); if (duplicates.Any()) { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs index 9394eb3b..f84ab225 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -107,7 +107,7 @@ public sealed class ApsAlert /// internal ApsAlert CopyAndValidate() { - return new ApsAlert() + var copy = new ApsAlert() { Title = this.Title, Subtitle = this.Subtitle, @@ -121,6 +121,19 @@ internal ApsAlert CopyAndValidate() ActionLocKey = this.ActionLocKey, LaunchImage = this.LaunchImage, }; + if (copy.TitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.TitleLocKey)) + { + throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs."); + } + if (copy.SubtitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.SubtitleLocKey)) + { + throw new ArgumentException("SubtitleLocKey is required when specifying SubtitleLocArgs."); + } + if (copy.LocArgs?.Any() == true && string.IsNullOrEmpty(copy.LocKey)) + { + throw new ArgumentException("LocKey is required when specifying LocArgs."); + } + return copy; } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 602d5c01..9c9b1f81 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -203,7 +203,7 @@ internal WebpushNotification CopyAndValidate() var json = serializer.Serialize(copy); var standardProperties = serializer.Deserialize>(json); var duplicates = customData.Keys - .Where(customProperty => standardProperties.ContainsKey(customProperty)) + .Where(customKey => standardProperties.ContainsKey(customKey)) .ToList(); if (duplicates.Any()) { From f4fe28b99a201e1033c8b42ec655f0bfe1dcfe45 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 17 Jan 2019 21:19:47 -0800 Subject: [PATCH 7/9] Adding more APNS tests --- .../Messaging/MessageTest.cs | 79 +++++++++++++------ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 2 +- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index e7b1837a..a5033d9b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -930,6 +930,22 @@ public void ApnsConfigDeserialization() Assert.Equal(original.Aps.AlertString, copy.Aps.AlertString); } + [Fact] + public void ApnsConfigCopy() + { + var original = new ApnsConfig() + { + Headers = new Dictionary(), + Aps = new Aps(), + CustomData = new Dictionary(), + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.NotSame(original.Headers, copy.Headers); + Assert.NotSame(original.Aps, copy.Aps); + Assert.NotSame(original.CustomData, copy.CustomData); + } + [Fact] public void ApnsConfigCustomApsDeserialization() { @@ -946,6 +962,8 @@ public void ApnsConfigCustomApsDeserialization() "aps", new Dictionary() { {"alert", "alert-text"}, + {"custom-key1", "custom-data"}, + {"custom-key2", true}, } }, {"custom-key3", "custom-data"}, @@ -958,6 +976,12 @@ public void ApnsConfigCustomApsDeserialization() original.CustomData.Remove("aps"); Assert.Equal(original.CustomData, copy.CustomData); Assert.Equal("alert-text", copy.Aps.AlertString); + var customApsData = new Dictionary() + { + {"custom-key1", "custom-data"}, + {"custom-key2", true}, + }; + Assert.Equal(customApsData, copy.Aps.CustomData); } [Fact] @@ -1155,6 +1179,38 @@ public void ApnsApsAlertMinimal() AssertJsonEquals(expected, message); } + [Fact] + public void ApsAlertDeserialization() + { + var original = new ApsAlert() + { + ActionLocKey = "action-key", + Body = "test-body", + LaunchImage = "test-image", + LocArgs = new List(){"arg1", "arg2"}, + LocKey = "loc-key", + Subtitle = "test-subtitle", + SubtitleLocArgs = new List(){"arg3", "arg4"}, + SubtitleLocKey = "subtitle-key", + Title = "test-title", + TitleLocArgs = new List(){"arg5", "arg6"}, + TitleLocKey = "title-key", + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.ActionLocKey, copy.ActionLocKey); + Assert.Equal(original.Body, copy.Body); + Assert.Equal(original.LaunchImage, copy.LaunchImage); + Assert.Equal(original.LocArgs, copy.LocArgs); + Assert.Equal(original.LocKey, copy.LocKey); + Assert.Equal(original.Subtitle, copy.Subtitle); + Assert.Equal(original.SubtitleLocArgs, copy.SubtitleLocArgs); + Assert.Equal(original.SubtitleLocKey, copy.SubtitleLocKey); + Assert.Equal(original.Title, copy.Title); + Assert.Equal(original.TitleLocArgs, copy.TitleLocArgs); + Assert.Equal(original.TitleLocKey, copy.TitleLocKey); + } + [Fact] public void ApnsApsAlertInvalidTitleLocArgs() { @@ -1465,27 +1521,4 @@ private void AssertJsonEquals(JObject expected, Message actual) $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } - - internal class Hello - { - - [JsonIgnore] - internal Aps Aps { get; set; } - - [JsonProperty("payload")] - internal ApnsPayload Payload - { - get - { - return new ApnsPayload() - { - Aps = this.Aps, - }; - } - set - { - this.Aps = value.Aps; - } - } - } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index a9b4efb9..39df3b4a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -102,7 +102,7 @@ internal ApnsConfig CopyAndValidate() } } - internal class ApnsPayload + internal sealed class ApnsPayload { [JsonProperty("aps")] public Aps Aps { get; set; } From ee8cfd3cf47225360d7db50c2ef34b7b4f7de99b Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 22 Jan 2019 16:21:15 -0800 Subject: [PATCH 8/9] Added more unit tests --- .../Messaging/MessageTest.cs | 45 +++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 11 ++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index a5033d9b..b211aa0b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -105,6 +105,13 @@ public void MessageDeserialization() { RestrictedPackageName = "test-pkg-name", }, + Apns = new ApnsConfig() + { + Aps = new Aps() + { + AlertString = "test-alert", + }, + }, Webpush = new WebpushConfig() { Data = new Dictionary(){{ "key", "value" }}, @@ -118,6 +125,7 @@ public void MessageDeserialization() Assert.Equal(original.Notification.Body, copy.Notification.Body); Assert.Equal( original.Android.RestrictedPackageName, copy.Android.RestrictedPackageName); + Assert.Equal(original.Apns.Aps.AlertString, copy.Apns.Aps.AlertString); Assert.Equal(original.Webpush.Data, copy.Webpush.Data); } @@ -130,6 +138,7 @@ public void MessageCopy() Data = new Dictionary(), Notification = new Notification(), Android = new AndroidConfig(), + Apns = new ApnsConfig(), Webpush = new WebpushConfig(), }; var copy = original.CopyAndValidate(); @@ -137,6 +146,7 @@ public void MessageCopy() Assert.NotSame(original.Data, copy.Data); Assert.NotSame(original.Notification, copy.Notification); Assert.NotSame(original.Android, copy.Android); + Assert.NotSame(original.Apns, copy.Apns); Assert.NotSame(original.Webpush, copy.Webpush); } @@ -1075,6 +1085,22 @@ public void ApnsCriticalSoundMinimal() AssertJsonEquals(expected, message); } + [Fact] + public void ApnsCriticalSoundDeserialization() + { + var original = new CriticalSound() + { + Name = "default", + Volume = 0.5, + Critical = true, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Name, copy.Name); + Assert.Equal(original.Volume.Value, copy.Volume.Value); + Assert.Equal(original.Critical, copy.Critical); + } + [Fact] public void ApnsApsAlert() { @@ -1211,6 +1237,25 @@ public void ApsAlertDeserialization() Assert.Equal(original.TitleLocKey, copy.TitleLocKey); } + [Fact] + public void ApsAlertCopy() + { + var original = new ApsAlert() + { + LocArgs = new List(){"arg1", "arg2"}, + LocKey = "loc-key", + SubtitleLocArgs = new List(){"arg3", "arg4"}, + SubtitleLocKey = "subtitle-key", + TitleLocArgs = new List(){"arg5", "arg6"}, + TitleLocKey = "title-key", + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.NotSame(original.LocArgs, copy.LocArgs); + Assert.NotSame(original.SubtitleLocArgs, copy.SubtitleLocArgs); + Assert.NotSame(original.TitleLocArgs, copy.TitleLocArgs); + } + [Fact] public void ApnsApsAlertInvalidTitleLocArgs() { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 39df3b4a..7a360b76 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -102,14 +102,21 @@ internal ApnsConfig CopyAndValidate() } } + /// + /// The APNs payload object as expected by the FCM backend service. + /// internal sealed class ApnsPayload { [JsonProperty("aps")] - public Aps Aps { get; set; } + internal Aps Aps { get; set; } [JsonExtensionData] - public IDictionary CustomData { get; set; } + internal IDictionary CustomData { get; set; } + /// + /// Copies this APNs payload, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. + /// internal ApnsPayload CopyAndValidate() { var copy = new ApnsPayload() From ae1e80c2c8309d6f406f4857318475775dabcb50 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 23 Jan 2019 13:50:31 -0800 Subject: [PATCH 9/9] Adding newline at eod --- FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 7a360b76..158016d2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -132,4 +132,4 @@ internal ApnsPayload CopyAndValidate() return copy; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index 585f4452..7c64dae9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -250,4 +250,4 @@ internal Aps CopyAndValidate() return copy; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs index f84ab225..c48c6012 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -136,4 +136,4 @@ internal ApsAlert CopyAndValidate() return copy; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index 636fe5b7..de59f1d0 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -88,4 +88,4 @@ internal CriticalSound CopyAndValidate() return copy; } } -} \ No newline at end of file +}