firebase-admin-sdk 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -21
  3. data/firebase-admin-sdk.gemspec +1 -0
  4. data/lib/firebase-admin-sdk.rb +21 -1
  5. data/lib/firebase/admin/app.rb +0 -6
  6. data/lib/firebase/admin/auth/client.rb +8 -0
  7. data/lib/firebase/admin/auth/token_verifier.rb +1 -1
  8. data/lib/firebase/admin/auth/user_info.rb +6 -6
  9. data/lib/firebase/admin/auth/user_manager.rb +2 -2
  10. data/lib/firebase/admin/auth/user_record.rb +5 -5
  11. data/lib/firebase/admin/config.rb +4 -2
  12. data/lib/firebase/admin/internal/http_client.rb +1 -0
  13. data/lib/firebase/admin/messaging/android_config.rb +77 -0
  14. data/lib/firebase/admin/messaging/android_fcm_options.rb +19 -0
  15. data/lib/firebase/admin/messaging/android_notification.rb +221 -0
  16. data/lib/firebase/admin/messaging/apns_config.rb +38 -0
  17. data/lib/firebase/admin/messaging/apns_fcm_options.rb +27 -0
  18. data/lib/firebase/admin/messaging/apns_payload.rb +28 -0
  19. data/lib/firebase/admin/messaging/aps.rb +82 -0
  20. data/lib/firebase/admin/messaging/aps_alert.rb +110 -0
  21. data/lib/firebase/admin/messaging/client.rb +181 -0
  22. data/lib/firebase/admin/messaging/critical_sound.rb +37 -0
  23. data/lib/firebase/admin/messaging/error.rb +36 -0
  24. data/lib/firebase/admin/messaging/error_info.rb +25 -0
  25. data/lib/firebase/admin/messaging/fcm_options.rb +19 -0
  26. data/lib/firebase/admin/messaging/light_settings.rb +34 -0
  27. data/lib/firebase/admin/messaging/message.rb +83 -0
  28. data/lib/firebase/admin/messaging/message_encoder.rb +355 -0
  29. data/lib/firebase/admin/messaging/multicast_message.rb +67 -0
  30. data/lib/firebase/admin/messaging/notification.rb +34 -0
  31. data/lib/firebase/admin/messaging/topic_management_response.rb +41 -0
  32. data/lib/firebase/admin/messaging/utils.rb +78 -0
  33. data/lib/firebase/admin/version.rb +1 -1
  34. metadata +36 -2
@@ -0,0 +1,37 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Critical alert sound configuration that can be included in an {APS}
5
+ class CriticalSound
6
+ # @return [String]
7
+ # The name of a sound file in the app's main bundle or in the `Library/Sounds` folder of the app's container
8
+ # directory. Specify the string "default" to play the system sound.
9
+ attr_accessor :name
10
+
11
+ # @return [Boolean, nil]
12
+ # The critical alert flag. Set to `true` to enable the critical alert.
13
+ attr_accessor :critical
14
+
15
+ # @return [Float, nil]
16
+ # The volume for the critical alert's sound. Must be a value between 0.0 (silent) and 1.0 (full volume).
17
+ attr_accessor :volume
18
+
19
+ # Initializes a {CriticalSound}.
20
+ #
21
+ # @param [String] name
22
+ # The name of a sound file in the app's main bundle or in the `Library/Sounds` folder of teh app's container
23
+ # directory.
24
+ # @param [Boolean, nil] critical
25
+ # The critical alert flag (optional).
26
+ # @param [Float, nil] volume
27
+ # The volume for the critical alert's sound (optional). Must be a value between 0.0 (silent) and 1.0 (full
28
+ # volume).
29
+ def initialize(name: "default", critical: nil, volume: nil)
30
+ self.name = name
31
+ self.critical = critical
32
+ self.volume = volume
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # A base class for errors raised by the admin sdk messaging client.
5
+ class Error < Firebase::Admin::Error
6
+ attr_reader :info
7
+
8
+ def initialize(msg, info = nil)
9
+ @info = info
10
+ super(msg)
11
+ end
12
+ end
13
+
14
+ # No more information is available about this error.
15
+ class UnspecifiedError < Error; end
16
+
17
+ # Request parameters were invalid.
18
+ class InvalidArgumentError < Error; end
19
+
20
+ # A message targeted to an iOS device or a web push registration could not be sent.
21
+ # Check the validity of your development and production credentials.
22
+ class ThirdPartyAuthError < Error; end
23
+
24
+ # This error can be caused by exceeded message rate quota, exceeded device message rate quota, or
25
+ # exceeded topic message rate quota.
26
+ class QuotaExceededError < Error; end
27
+
28
+ # The authenticated sender ID is different from the sender ID for the registration token.
29
+ class SenderIdMismatchError < Error; end
30
+
31
+ # App instance was unregistered from FCM. This usually means that the token used is no longer valid and
32
+ # a new one must be used.
33
+ class UnregisteredError < Error; end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Information on an error encountered when performing a topic management operation.
5
+ class ErrorInfo
6
+ # @return [Integer] The index of the registration token the error is related to.
7
+ attr_accessor :index
8
+
9
+ # @return [String] The description of the error encountered.
10
+ attr_accessor :reason
11
+
12
+ # Initializes an {ErrorInfo}.
13
+ #
14
+ # @param [Integer] index
15
+ # The index of the registration token the error is related to.
16
+ # @param [String] reason
17
+ # The description of the error encountered.
18
+ def initialize(index:, reason:)
19
+ self.index = index
20
+ self.reason = reason
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Represents options for features provided by the FCM SDK.
5
+ class FCMOptions
6
+ # @return [String, nil] Label associated with the message's analytics data.
7
+ attr_accessor :analytics_label
8
+
9
+ # Initializes an {FCMOptions}.
10
+ #
11
+ # @param [String, nil] analytics_label
12
+ # The label associated with the message's analytics data (optional).
13
+ def initialize(analytics_label: nil)
14
+ self.analytics_label = analytics_label
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Represents settings to control notification LED that can be included in an {AndroidNotification}.
5
+ class LightSettings
6
+ # @return [String]
7
+ # Sets color of the LED in `#rrggbb` or `#rrggbbaa` format.
8
+ attr_accessor :color
9
+
10
+ # @return [Numeric]
11
+ # Along with {light_off_duration}, defines the blink rate of LED flashes.
12
+ attr_accessor :light_on_duration
13
+
14
+ # @return [Numeric]
15
+ # Along with {light_on_duration}, defines the blink rate of LED flashes.
16
+ attr_accessor :light_off_duration
17
+
18
+ # Initializes a {LightSettings}.
19
+ #
20
+ # @param [String] color
21
+ # The color of the LED in `#rrggbb` or `#rrggbbaa` format.
22
+ # @param [Numeric] light_on_duration
23
+ # Along with {light_off_duration}, defines the blink rate of LED flashes.
24
+ # @param [Numeric] light_off_duration
25
+ # Along with {light_on_duration}, defines the blink rate of LED flashes.
26
+ def initialize(color:, light_on_duration:, light_off_duration:)
27
+ self.color = color
28
+ self.light_on_duration = light_on_duration
29
+ self.light_off_duration = light_off_duration
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,83 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # A message that can be sent via Firebase Cloud Messaging.
5
+ #
6
+ # Contains payload information as well as recipient information. In particular, the message must contain exactly
7
+ # one of token, topic or condition fields.
8
+ class Message
9
+ # @return [Hash<String, String>, nil]
10
+ # A hash of data fields (optional). All keys and values must be strings.
11
+ attr_accessor :data
12
+
13
+ # @return [Notification, nil]
14
+ # A {Notification} (optional).
15
+ attr_accessor :notification
16
+
17
+ # @return [AndroidConfig, nil]
18
+ # An {AndroidConfig} (optional).
19
+ attr_accessor :android
20
+
21
+ # @return [APNSConfig, nil]
22
+ # An {APNSConfig} (optional).
23
+ attr_accessor :apns
24
+
25
+ # @return [FCMOptions, nil]
26
+ # An {FCMOptions} (optional).
27
+ attr_accessor :fcm_options
28
+
29
+ # @return [String, nil]
30
+ # Registration token of the device to which the message should be sent (optional).
31
+ attr_accessor :token
32
+
33
+ # @return [String, nil]
34
+ # Name of the FCM topic to which the message should be sent (optional). Topic name may contain the `/topics/`
35
+ # prefix.
36
+ attr_accessor :topic
37
+
38
+ # @return [String, nil]
39
+ # The FCM condition to which the message should be sent (optional).
40
+ attr_accessor :condition
41
+
42
+ # Initializes a {Message}.
43
+ #
44
+ # @param [Hash<String, String>, nil] data
45
+ # A hash of data fields (optional). All keys and values must be strings.
46
+ # @param [Notification, nil] notification
47
+ # A {Notification} (optional).
48
+ # @param [AndroidConfig, nil] android
49
+ # An {AndroidConfig} (optional).
50
+ # @param [APNSConfig, nil] apns
51
+ # An {APNSConfig} (optional).
52
+ # @param [FCMOptions, nil] fcm_options
53
+ # An {FCMOptions} (optional).
54
+ # @param [String, nil] token
55
+ # A registration token of the device to send the message to (optional).
56
+ # @param [String, nil] topic
57
+ # The name of the FCM topic to send the message to (optional).
58
+ # The topic name may contain the `/topics/` prefix.
59
+ # @param [String, nil] condition
60
+ # The FCM condition to which the message should be sent (optional).
61
+ def initialize(
62
+ data: nil,
63
+ notification: nil,
64
+ android: nil,
65
+ apns: nil,
66
+ fcm_options: nil,
67
+ token: nil,
68
+ topic: nil,
69
+ condition: nil
70
+ )
71
+ self.data = data
72
+ self.notification = notification
73
+ self.android = android
74
+ self.apns = apns
75
+ self.fcm_options = fcm_options
76
+ self.token = token
77
+ self.topic = topic
78
+ self.condition = condition
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,355 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ class MessageEncoder
5
+ # Encodes a {Message}.
6
+ #
7
+ # @param [Message] message
8
+ # The message to encode.
9
+ # @return [Hash]
10
+ def encode(message)
11
+ raise ArgumentError, "message must be a Message" unless message.is_a?(Message)
12
+ result = {
13
+ android: encode_android(message.android),
14
+ apns: encode_apns(message.apns),
15
+ condition: check_string("Message.condition", message.condition, non_empty: true),
16
+ data: check_string_hash("Message.data", message.data),
17
+ notification: encode_notification(message.notification),
18
+ token: check_string("Message.token", message.token, non_empty: true),
19
+ topic: check_string("Message.topic", message.topic, non_empty: true),
20
+ fcm_options: encode_fcm_options(message.fcm_options)
21
+ }
22
+ result[:topic] = sanitize_topic_name(result[:topic])
23
+ result = remove_nil_values(result)
24
+ unless result.count { |k, _| [:token, :topic, :condition].include?(k) } == 1
25
+ raise ArgumentError, "Exactly one token, topic or condition must be specified"
26
+ end
27
+ result
28
+ end
29
+
30
+ # @return [String, nil]
31
+ def sanitize_topic_name(topic, strip_prefix: true)
32
+ return nil unless topic
33
+ prefix = "/topics/"
34
+ if topic.start_with?(prefix)
35
+ topic = topic[prefix.length..]
36
+ end
37
+ unless /\A[a-zA-Z0-9\-_.~%]+\Z/.match?(topic)
38
+ raise ArgumentError, "Malformed topic name."
39
+ end
40
+ strip_prefix ? topic : "/topics/#{topic}"
41
+ end
42
+
43
+ private
44
+
45
+ # @return [Hash, nil]
46
+ def encode_android(v)
47
+ return nil unless v
48
+ raise ArgumentError, "Message.android must be an AndroidConfig." unless v.is_a?(AndroidConfig)
49
+ result = {
50
+ collapse_key: check_string("AndroidConfig.collapse_key", v.collapse_key),
51
+ data: check_string_hash("AndroidConfig.data", v.data),
52
+ notification: encode_android_notification(v.notification),
53
+ priority: check_string("AndroidConfig.priority", v.priority, non_empty: true),
54
+ restricted_package_name: check_string("AndroidConfig.restricted_package_name", v.restricted_package_name),
55
+ ttl: encode_duration("AndroidConfig.ttl", v.ttl),
56
+ fcm_options: encode_android_fcm_options(v.fcm_options)
57
+ }
58
+ result = remove_nil_values(result)
59
+ if result.key?(:priority) && !%w[normal high].include?(result[:priority])
60
+ raise ArgumentError, "AndroidConfig.priority must be 'normal' or 'high'"
61
+ end
62
+ result
63
+ end
64
+
65
+ # @return [Hash, nil]
66
+ def encode_android_notification(v)
67
+ return nil unless v
68
+ unless v.is_a?(AndroidNotification)
69
+ raise ArgumentError, "AndroidConfig.notification must be an AndroidNotification"
70
+ end
71
+
72
+ result = {
73
+ body: check_string("AndroidNotification.body", v.body),
74
+ body_loc_key: check_string("AndroidNotification.body_loc_key", v.body_loc_key),
75
+ body_loc_args: check_string_array("AndroidNotification.body_loc_args", v.body_loc_args),
76
+ click_action: check_string("AndroidNotification.click_action", v.click_action),
77
+ color: check_color("AndroidNotification.color", v.color, allow_alpha: true, required: false),
78
+ icon: check_string("AndroidNotification.icon", v.icon),
79
+ sound: check_string("AndroidNotification.sound", v.sound),
80
+ tag: check_string("AndroidNotification.tag", v.tag),
81
+ title: check_string("AndroidNotification.title", v.title),
82
+ title_loc_key: check_string("AndroidNotification.title_loc_key", v.title_loc_key),
83
+ title_loc_args: check_string_array("AndroidNotification.title_loc_args", v.title_loc_args),
84
+ channel_id: check_string("AndroidNotification.channel_id", v.channel_id),
85
+ image: check_string("AndroidNotification.image", v.image),
86
+ ticker: check_string("AndroidNotification.ticker", v.ticker),
87
+ sticky: v.sticky,
88
+ event_time: check_time("AndroidNotification.event_time", v.event_time),
89
+ local_only: v.local_only,
90
+ notification_priority: check_string("AndroidNotification.priority", v.priority, non_empty: true),
91
+ vibrate_timings: check_numeric_array("AndroidNotification.vibrate_timings", v.vibrate_timings),
92
+ default_vibrate_timings: v.default_vibrate_timings,
93
+ default_sound: v.default_sound,
94
+ default_light_settings: v.default_light_settings,
95
+ light_settings: encode_light_settings(v.light_settings),
96
+ visibility: check_string("AndroidNotification.visibility", v.visibility, non_empty: true),
97
+ notification_count: check_numeric("AndroidNotification.notification_count", v.notification_count)
98
+ }
99
+ result = remove_nil_values(result)
100
+
101
+ if result.key?(:body_loc_args) && !result.key?(:body_loc_key)
102
+ raise ArgumentError, "AndroidNotification.body_loc_key is required when specifying body_loc_args"
103
+ elsif result.key?(:title_loc_args) && !result.key?(:title_loc_key)
104
+ raise ArgumentError, "AndroidNotification.title_loc_key is required when specifying title_loc_args"
105
+ end
106
+
107
+ if (event_time = result[:event_time])
108
+ event_time = event_time.dup.utc unless event_time.utc?
109
+ result[:event_time] = event_time.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
110
+ end
111
+
112
+ if (priority = result[:notification_priority])
113
+ unless %w[min low default high max].include?(priority)
114
+ raise ArgumentError, "AndroidNotification.priority must be 'default', 'min', 'low', 'high' or 'max'."
115
+ end
116
+ result[:notification_priority] = "PRIORITY_#{priority.upcase}"
117
+ end
118
+
119
+ if (visibility = result[:visibility])
120
+ unless %w[private public secret].include?(visibility)
121
+ raise ArgumentError, "AndroidNotification.visibility must be 'private', 'public' or 'secret'"
122
+ end
123
+ result[:visibility] = visibility.upcase
124
+ end
125
+
126
+ if (vibrate_timings = result[:vibrate_timings])
127
+ vibrate_timing_strings = vibrate_timings.map do |t|
128
+ encode_duration("AndroidNotification.vibrate_timings", t)
129
+ end
130
+ result[:vibrate_timings] = vibrate_timing_strings
131
+ end
132
+
133
+ result
134
+ end
135
+
136
+ # @return [Hash, nil]
137
+ def encode_android_fcm_options(v)
138
+ return nil unless v
139
+ unless v.is_a?(AndroidFCMOptions)
140
+ raise ArgumentError, "AndroidConfig.fcm_options must be an AndroidFCMOptions"
141
+ end
142
+ result = {
143
+ analytics_label: check_analytics_label("AndroidFCMOptions.analytics_label", v.analytics_label)
144
+ }
145
+ remove_nil_values(result)
146
+ end
147
+
148
+ # @return [String, nil]
149
+ def encode_duration(label, value)
150
+ return nil unless value
151
+ raise ArgumentError, "#{label} must be a numeric duration in seconds" unless value.is_a?(Numeric)
152
+ raise ArgumentError, "#{label} must not be negative" if value < 0
153
+ to_seconds_string(value)
154
+ end
155
+
156
+ # @return [Hash, nil]
157
+ def encode_light_settings(v)
158
+ return nil unless v
159
+ raise ArgumentError, "AndroidNotification.light_settings must be a LightSettings." unless v.is_a?(LightSettings)
160
+ result = {
161
+ color: encode_color("LightSettings.color", v.color, allow_alpha: true),
162
+ light_on_duration: encode_duration("LightSettings.light_on_duration", v.light_on_duration),
163
+ light_off_duration: encode_duration("LightSettings.light_off_duration", v.light_off_duration)
164
+ }
165
+ result = remove_nil_values(result)
166
+ unless result.key?(:light_on_duration)
167
+ raise ArgumentError, "LightSettings.light_on_duration is required"
168
+ end
169
+ unless result.key?(:light_off_duration)
170
+ raise ArgumentError, "LightSettings.light_off_duration is required"
171
+ end
172
+ result
173
+ end
174
+
175
+ # @return [Hash]
176
+ def encode_color(label, value, allow_alpha: false)
177
+ value = check_color(label, value, allow_alpha: allow_alpha, required: true)
178
+ value += "FF" if value&.length == 7
179
+ r = value[1..2].to_i(16) / 255.0
180
+ g = value[3..4].to_i(16) / 255.0
181
+ b = value[5..6].to_i(16) / 255.0
182
+ a = value[7..8].to_i(16) / 255.0
183
+ {red: r, green: g, blue: b, alpha: a}
184
+ end
185
+
186
+ # @return [Hash, nil]
187
+ def encode_apns(apns)
188
+ return nil unless apns
189
+ raise ArgumentError, "Message.apns must be an APNSConfig" unless apns.is_a?(APNSConfig)
190
+ result = {
191
+ headers: check_string_hash("APNSConfig.headers", apns.headers),
192
+ payload: encode_apns_payload(apns.payload),
193
+ fcm_options: encode_apns_fcm_options(apns.fcm_options)
194
+ }
195
+ remove_nil_values(result)
196
+ end
197
+
198
+ # @return [Hash, nil]
199
+ def encode_apns_payload(payload)
200
+ return nil unless payload
201
+ raise ArgumentError, "APNSConfig.payload must be an APNSPayload" unless payload.is_a?(APNSPayload)
202
+ result = {
203
+ aps: encode_aps(payload.aps)
204
+ }
205
+ payload.data&.each do |k, v|
206
+ result[k] = v
207
+ end
208
+ remove_nil_values(result)
209
+ end
210
+
211
+ # @return [Hash, nil]
212
+ def encode_apns_fcm_options(options)
213
+ return nil unless options
214
+ raise ArgumentError, "APNSConfig.fcm_options must be an APNSFCMOptions" unless options.is_a?(APNSFCMOptions)
215
+ result = {
216
+ analytics_label: check_analytics_label("APNSFCMOptions.analytics_label", options.analytics_label),
217
+ image: check_string("APNSFCMOptions.image", options.image)
218
+ }
219
+ remove_nil_values(result)
220
+ end
221
+
222
+ # @return [Hash]
223
+ def encode_aps(aps)
224
+ raise ArgumentError, "APNSPayload.aps is required" unless aps
225
+ raise ArgumentError, "APNSPayload.aps must be an APS" unless aps.is_a?(APS)
226
+ result = {
227
+ alert: encode_aps_alert(aps.alert),
228
+ badge: check_numeric("APS.badge", aps.badge),
229
+ sound: encode_aps_sound(aps.sound),
230
+ category: check_string("APS.category", aps.category),
231
+ "thread-id": check_string("APS.thread_id", aps.thread_id)
232
+ }
233
+
234
+ result[:"content-available"] = 1 if aps.content_available
235
+ result[:"mutable-content"] = 1 if aps.mutable_content
236
+
237
+ if (custom_data = aps.custom_data)
238
+ raise ArgumentError, "APS.custom_data must be a hash" unless custom_data.is_a?(Hash)
239
+ custom_data.each do |k, v|
240
+ unless k.is_a?(String) || k.is_a?(Symbol)
241
+ raise ArgumentError, "APS.custom_data key #{k}, must be a string or symbol"
242
+ end
243
+ k = k.to_sym
244
+ raise ArgumentError, "Multiple specifications for #{k} in APS" if result.key?(k)
245
+ result[k] = v
246
+ end
247
+ end
248
+
249
+ remove_nil_values(result)
250
+ end
251
+
252
+ # @return [Hash, String, nil]
253
+ def encode_aps_alert(alert)
254
+ return nil unless alert
255
+ return alert if alert.is_a?(String)
256
+ raise ArgumentError, "APS.alert must be a string or an an APSAlert" unless alert.is_a?(APSAlert)
257
+
258
+ result = {
259
+ title: check_string("APSAlert.title", alert.title),
260
+ subtitle: check_string("APSAlert.subtitle", alert.subtitle),
261
+ body: check_string("APSAlert.body", alert.body),
262
+ "title-loc-key": check_string("APSAlert.title_loc_key", alert.title_loc_key),
263
+ "title-loc-args": check_string_array("APSAlert.title_loc_args", alert.title_loc_args),
264
+ "subtitle-loc-key": check_string("APSAlert.subtitle_loc_key", alert.subtitle_loc_key),
265
+ "subtitle-loc-args": check_string_array("APSAlert.subtitle_loc_args", alert.subtitle_loc_args),
266
+ "loc-key": check_string("APSAlert.loc_key", alert.loc_key),
267
+ "loc-args": check_string_array("ASPAlert.loc_args", alert.loc_args),
268
+ "action-loc-key": check_string("APSAlert.action_loc_key", alert.action_loc_key),
269
+ "launch-image": check_string("APSAlert.launch_image", alert.launch_image)
270
+ }
271
+ result = remove_nil_values(result)
272
+
273
+ if result.key?(:"loc-args") && !result.key?(:"loc-key")
274
+ raise ArgumentError, "APSAlert.loc_key is required when specifying loc_args"
275
+ elsif result.key?(:"title-loc-args") && !result.key?(:"title-loc-key")
276
+ raise ArgumentError, "APSAlert.title_loc_key is required when specifying title_loc_args"
277
+ elsif result.key?(:"subtitle-loc-args") && !result.key?(:"subtitle-loc-key")
278
+ raise ArgumentError, "APSAlert.subtitle_loc_key is required when specifying subtitle_loc_args"
279
+ end
280
+
281
+ if (custom_data = alert.custom_data)
282
+ raise ArgumentError, "APSAlert.custom_data must be a hash" unless custom_data.is_a?(Hash)
283
+ custom_data.each do |k, v|
284
+ unless k.is_a?(String) || k.is_a?(Symbol)
285
+ raise ArgumentError, "APSAlert.custom_data key #{k}, must be a string or symbol"
286
+ end
287
+ k = k.to_sym
288
+ result[k] = v
289
+ end
290
+ end
291
+ remove_nil_values(result)
292
+ end
293
+
294
+ # @return [Hash, String, nil]
295
+ def encode_aps_sound(sound)
296
+ return nil unless sound
297
+ return sound if sound.is_a?(String) && !sound.empty?
298
+ unless sound.is_a?(CriticalSound)
299
+ raise ArgumentError, "APS.sound must be a non-empty string or a CriticalSound"
300
+ end
301
+
302
+ result = {
303
+ name: check_string("CriticalSound.name", sound.name, non_empty: true),
304
+ volume: check_numeric("CriticalSound.volume", sound.volume)
305
+ }
306
+
307
+ result[:critical] = 1 if sound.critical
308
+ raise ArgumentError, "CriticalSound.name is required" if result[:name].nil?
309
+
310
+ if (volume = result[:volume])
311
+ raise ArgumentError, "CriticalSound.volume must be between [0,1]." unless volume >= 0 && volume <= 1
312
+ end
313
+
314
+ remove_nil_values(result)
315
+ end
316
+
317
+ # @return [Hash, nil]
318
+ def encode_notification(v)
319
+ return nil unless v
320
+ raise ArgumentError, "Message.notification must be a Notification" unless v.is_a?(Notification)
321
+ result = {
322
+ body: check_string("Notification.body", v.body),
323
+ title: check_string("Notification.title", v.title),
324
+ image: check_string("Notification.image", v.image)
325
+ }
326
+ remove_nil_values(result)
327
+ end
328
+
329
+ # @return [Hash, nil]
330
+ def encode_fcm_options(options)
331
+ return nil unless options
332
+ raise ArgumentError, "Message.fcm_options must be a FCMOptions." unless options.is_a?(FCMOptions)
333
+ result = {
334
+ analytics_label: check_analytics_label("Message.fcm_options", options.analytics_label)
335
+ }
336
+ remove_nil_values(result)
337
+ end
338
+
339
+ # Remove nil values and empty collections from the specified hash.
340
+ # @return [Hash]
341
+ def remove_nil_values(hash)
342
+ hash.reject do |_, v|
343
+ if v.is_a?(Hash) || v.is_a?(Array)
344
+ v.empty?
345
+ else
346
+ v.nil?
347
+ end
348
+ end
349
+ end
350
+
351
+ include Utils
352
+ end
353
+ end
354
+ end
355
+ end