expo-server-sdk 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitattributes +1 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +18 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +68 -0
- data/LICENSE.txt +21 -0
- data/README.md +252 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/expo-server-sdk.gemspec +41 -0
- data/lib/expo/server/sdk/version.rb +15 -0
- data/lib/expo/server/sdk.rb +14 -0
- data/lib/push/chunk.rb +58 -0
- data/lib/push/client.rb +292 -0
- data/lib/push/notification.rb +348 -0
- data/lib/push/receipts.rb +98 -0
- data/lib/push/tickets.rb +118 -0
- metadata +96 -0
data/lib/push/client.rb
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'connection_pool'
|
4
|
+
require 'http'
|
5
|
+
|
6
|
+
require_relative './chunk'
|
7
|
+
require_relative './notification'
|
8
|
+
require_relative './receipts'
|
9
|
+
require_relative './tickets'
|
10
|
+
|
11
|
+
module Expo
|
12
|
+
module Push # rubocop:disable Style/Documentation
|
13
|
+
class Error < StandardError # rubocop:disable Style/Documentation
|
14
|
+
def self.explain(error) # rubocop:disable Metrics/MethodLength
|
15
|
+
identifier = error.is_a?(String) ? error : error.fetch('details').fetch('error')
|
16
|
+
|
17
|
+
case identifier
|
18
|
+
when 'DeviceNotRegistered'
|
19
|
+
'The device cannot receive push notifications anymore and you' \
|
20
|
+
' should stop sending messages to the corresponding Expo push token.'
|
21
|
+
when 'InvalidCredentials'
|
22
|
+
'Your push notification credentials for your standalone app are ' \
|
23
|
+
'invalid (ex: you may have revoked them). Run expo build:ios -c ' \
|
24
|
+
'to regenerate new push notification credentials for iOS. If you ' \
|
25
|
+
'revoke an APN key, all apps that rely on that key will no longer ' \
|
26
|
+
'be able to send or receive push notifications until you upload a ' \
|
27
|
+
'new key to replace it. Uploading a new APN key will not change ' \
|
28
|
+
'your users\' Expo Push Tokens.'
|
29
|
+
when 'MessageTooBig'
|
30
|
+
'The total notification payload was too large. On Android and iOS ' \
|
31
|
+
'the total payload must be at most 4096 bytes.'
|
32
|
+
when 'MessageRateExceeded'
|
33
|
+
'You are sending messages too frequently to the given device. ' \
|
34
|
+
'Implement exponential backoff and slowly retry sending messages.'
|
35
|
+
else
|
36
|
+
"There is no embedded explanation for #{identifier}. Sorry!"
|
37
|
+
end
|
38
|
+
rescue KeyError
|
39
|
+
'There is no identifier given to explain'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ServerError < Error; end
|
44
|
+
|
45
|
+
class ArgumentError < Error; end
|
46
|
+
|
47
|
+
class TicketsWithErrors < Error # rubocop:disable Style/Documentation
|
48
|
+
attr_reader :data, :errors
|
49
|
+
|
50
|
+
def initialize(errors:, data:)
|
51
|
+
self.data = data
|
52
|
+
self.errors = errors
|
53
|
+
|
54
|
+
if errors.length.zero?
|
55
|
+
super 'Expected at least one error, but got none'
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
puts errors
|
60
|
+
|
61
|
+
super "Expo indicated one or more problems: #{errors.map { |error| error['message'] }}"
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_writer :data, :errors
|
67
|
+
end
|
68
|
+
|
69
|
+
class TicketsExpectationFailed < Error # rubocop:disable Style/Documentation
|
70
|
+
attr_reader :data
|
71
|
+
|
72
|
+
def initialize(expected_count:, data:)
|
73
|
+
self.data = data
|
74
|
+
|
75
|
+
super format(
|
76
|
+
"Expected %<count>s ticket#{if expected_count != 1
|
77
|
+
's'
|
78
|
+
end}, actual: %<actual>s. The response data can be inspected.",
|
79
|
+
count: expected_count,
|
80
|
+
actual: data.is_a?(Array) ? data.length : '<not a list of tickets>'
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
attr_writer :data
|
87
|
+
end
|
88
|
+
|
89
|
+
class ReceiptsWithErrors < Error # rubocop:disable Style/Documentation
|
90
|
+
attr_reader :data, :errors
|
91
|
+
|
92
|
+
def initialize(errors:, data:)
|
93
|
+
self.data = data
|
94
|
+
self.errors = errors
|
95
|
+
|
96
|
+
if errors.length.zero?
|
97
|
+
super 'Expected at least one error, but got none'
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
super "Expo indicated one or more problems: #{errors.map { |error| error[:message] }}"
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
attr_writer :data, :errors
|
107
|
+
end
|
108
|
+
|
109
|
+
class PushTokenInvalid < Error # rubocop:disable Style/Documentation
|
110
|
+
attr_reader :token
|
111
|
+
|
112
|
+
def initialize(token:)
|
113
|
+
self.token = token
|
114
|
+
|
115
|
+
super "Expected a valid Expo Push Token, actual: #{token}"
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
attr_writer :token
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# The max number of push notifications to be sent at once. Since we can't automatically upgrade
|
125
|
+
# everyone using this library, we should strongly try not to decrease it.
|
126
|
+
#
|
127
|
+
PUSH_NOTIFICATION_CHUNK_LIMIT = 100
|
128
|
+
|
129
|
+
##
|
130
|
+
# The max number of push notification receipts to request at once.
|
131
|
+
#
|
132
|
+
PUSH_NOTIFICATION_RECEIPT_CHUNK_LIMIT = 300
|
133
|
+
|
134
|
+
##
|
135
|
+
# The default max number of concurrent HTTP requests to send at once and spread out the load,
|
136
|
+
# increasing the reliability of notification delivery.
|
137
|
+
#
|
138
|
+
DEFAULT_CONCURRENT_REQUEST_LIMIT = 6
|
139
|
+
|
140
|
+
BASE_URL = 'https://exp.host'
|
141
|
+
BASE_API_URL = '/--/api/v2'
|
142
|
+
|
143
|
+
PUSH_API_URL = "#{BASE_API_URL}/push/send"
|
144
|
+
RECEIPTS_API_URL = "#{BASE_API_URL}/push/getReceipts"
|
145
|
+
|
146
|
+
##
|
147
|
+
# Returns `true` if the token is an Expo push token
|
148
|
+
#
|
149
|
+
def self.expo_push_token?(token)
|
150
|
+
return false unless token
|
151
|
+
|
152
|
+
/\AExpo(?:nent)?PushToken\[[^\]]+\]\z/.match?(token) ||
|
153
|
+
/\A[a-z\d]{8}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{4}-[a-z\d]{12}\z/i.match?(token)
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# This is the Push Client for Expo's Push Service. It is responsible for
|
158
|
+
# sending the notifications themselves as well as retrieving the receipts.
|
159
|
+
#
|
160
|
+
# It will attempt to keep a persistent connection once the first request is
|
161
|
+
# made, and allow at most {concurrency} concurrent requests.
|
162
|
+
#
|
163
|
+
class Client
|
164
|
+
def initialize(
|
165
|
+
access_token: nil,
|
166
|
+
concurrency: DEFAULT_CONCURRENT_REQUEST_LIMIT,
|
167
|
+
logger: false,
|
168
|
+
instrumentation: false
|
169
|
+
)
|
170
|
+
self.access_token = access_token
|
171
|
+
self.concurrency = concurrency
|
172
|
+
self.logger = logger
|
173
|
+
self.instrumentation = if instrumentation == true
|
174
|
+
{ instrumentation: ActiveSupport::Notifications.instrumenter }
|
175
|
+
else
|
176
|
+
instrumentation
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
181
|
+
def send(notifications)
|
182
|
+
connect unless pool?
|
183
|
+
|
184
|
+
threads = Chunk.for(notifications).map do |chunk|
|
185
|
+
expected_count = chunk.count
|
186
|
+
|
187
|
+
Thread.new do
|
188
|
+
pool.with do |http|
|
189
|
+
response = http.post(PUSH_API_URL, json: chunk.as_json)
|
190
|
+
parsed_response = response.parse
|
191
|
+
|
192
|
+
data = parsed_response['data']
|
193
|
+
errors = parsed_response['errors']
|
194
|
+
|
195
|
+
if errors&.length&.positive?
|
196
|
+
TicketsWithErrors.new(data: data, errors: errors)
|
197
|
+
elsif !data.is_a?(Array) || data.length != expected_count
|
198
|
+
TicketsExpectationFailed.new(expected_count: expected_count, data: data)
|
199
|
+
else
|
200
|
+
data.map { |ticket| Ticket.new(ticket) }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
Tickets.new(threads.map(&:value))
|
207
|
+
end
|
208
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
209
|
+
|
210
|
+
def send!(notifications)
|
211
|
+
send(notifications).tap do |result|
|
212
|
+
result.each_error do |error| # rubocop:disable Lint/UnreachableLoop
|
213
|
+
raise error
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
219
|
+
def receipts(receipt_ids)
|
220
|
+
connect unless pool?
|
221
|
+
|
222
|
+
pool.with do |http|
|
223
|
+
response = http.post(RECEIPTS_API_URL, json: { ids: Array(receipt_ids) })
|
224
|
+
parsed_response = response.parse
|
225
|
+
|
226
|
+
if !parsed_response || parsed_response.is_a?(Array) || !parsed_response.is_a?(Hash)
|
227
|
+
raise ServerError, 'Expected hash with receipt id => receipt, but got some other data structure'
|
228
|
+
end
|
229
|
+
|
230
|
+
errors = parsed_response['errors']
|
231
|
+
|
232
|
+
if errors&.length&.positive?
|
233
|
+
ReceiptsWithErrors.new(data: parsed_response, errors: errors)
|
234
|
+
else
|
235
|
+
results = parsed_response.map do |receipt_id, data|
|
236
|
+
Receipt.new(data: data, receipt_id: receipt_id)
|
237
|
+
end
|
238
|
+
|
239
|
+
Receipts.new(results: results, requested_ids: receipt_ids)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
244
|
+
|
245
|
+
def connect # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
246
|
+
shutdown
|
247
|
+
|
248
|
+
self.pool = ConnectionPool.new(size: concurrency, timeout: 5) do
|
249
|
+
http = HTTP.headers(
|
250
|
+
# All request should return JSON (in this client)
|
251
|
+
Accept: 'application/json',
|
252
|
+
# All responses are allowed to be gzip-encoded
|
253
|
+
'Accept-Encoding': 'gzip',
|
254
|
+
# Set user-agent so that expo can track usage
|
255
|
+
'User-Agent': format('expo-server-sdk-ruby/%<version>s', version: VERSION)
|
256
|
+
)
|
257
|
+
|
258
|
+
http = http.headers('Authorization', "Bearer #{access_token}") if access_token
|
259
|
+
|
260
|
+
# All requests are allowed to automatically gzip
|
261
|
+
http = http.use(:auto_inflate)
|
262
|
+
# Turn on logging if there is a logger
|
263
|
+
http = http.use(logging: { logger: logger }) if logger
|
264
|
+
# Turn on instrumentation
|
265
|
+
http = http.use(instrumentation: instrumentation) if instrumentation
|
266
|
+
|
267
|
+
http.persistent(BASE_URL)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def shutdown
|
272
|
+
return unless pool?
|
273
|
+
|
274
|
+
pool.shutdown do |conn|
|
275
|
+
conn&.close
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def notification
|
280
|
+
Expo::Push::Notification.new
|
281
|
+
end
|
282
|
+
|
283
|
+
private
|
284
|
+
|
285
|
+
attr_accessor :access_token, :concurrency, :pool, :logger, :instrumentation
|
286
|
+
|
287
|
+
def pool?
|
288
|
+
!!pool
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,348 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Expo
|
4
|
+
module Push
|
5
|
+
##
|
6
|
+
# Data model for PushNotification.
|
7
|
+
#
|
8
|
+
class Notification # rubocop:disable Metrics/ClassLength
|
9
|
+
attr_accessor :recipients
|
10
|
+
|
11
|
+
def self.to(recipient)
|
12
|
+
new.to(recipient)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(_recipient = [])
|
16
|
+
self.recipients = []
|
17
|
+
self._params = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Set or add recipient or recipients.
|
22
|
+
#
|
23
|
+
# Must be a valid Expo Push Token, or array-like / enumerator that yield
|
24
|
+
# valid Expo Push Tokens, or an PushTokenInvalid error is raised.
|
25
|
+
#
|
26
|
+
# @see PushTokenInvalid
|
27
|
+
# @see #<<
|
28
|
+
#
|
29
|
+
def to(recipient_or_multiple)
|
30
|
+
Array(recipient_or_multiple).each do |recipient|
|
31
|
+
self << recipient
|
32
|
+
end
|
33
|
+
|
34
|
+
self
|
35
|
+
rescue NoMethodError
|
36
|
+
raise ArgumentError, 'to must be a single Expo Push Token, or an array-like/enumerator of Expo Push Tokens'
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Set or overwrite the data.
|
41
|
+
#
|
42
|
+
# Data must be a Hash, or at least be JSON serializable as hash.
|
43
|
+
#
|
44
|
+
# A JSON object delivered to your app. It may be up to about 4KiB; the
|
45
|
+
# total notification payload sent to Apple and Google must be at most
|
46
|
+
# 4KiB or else you will get a "Message Too Big" error.
|
47
|
+
#
|
48
|
+
def data(value)
|
49
|
+
json_data = value.respond_to?(:as_json) ? value.as_json : value.to_h
|
50
|
+
|
51
|
+
raise ArgumentError, 'data must be hash-like or nil' if !json_data.nil? && !json_data.is_a?(Hash)
|
52
|
+
|
53
|
+
_params[:data] = json_data
|
54
|
+
self
|
55
|
+
rescue NoMethodError
|
56
|
+
raise ArgumentError, 'data must be hash-like, respond to as_json, or nil'
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Set or overwrite the title.
|
61
|
+
#
|
62
|
+
# The title to display in the notification. Often displayed above the
|
63
|
+
# notification body.
|
64
|
+
#
|
65
|
+
def title(value)
|
66
|
+
_params[:title] = value.nil? ? nil : String(value)
|
67
|
+
self
|
68
|
+
rescue NoMethodError
|
69
|
+
raise ArgumentError, 'title must be nil or string-like'
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Set or overwrite the subtitle.
|
74
|
+
#
|
75
|
+
# The subtitle to display in the notification below the title.
|
76
|
+
#
|
77
|
+
# @note iOS only
|
78
|
+
#
|
79
|
+
def subtitle(value)
|
80
|
+
_params[:subtitle] = value.nil? ? nil : String(value)
|
81
|
+
self
|
82
|
+
rescue NoMethodError
|
83
|
+
raise ArgumentError, 'subtitle must be nil or string-like'
|
84
|
+
end
|
85
|
+
|
86
|
+
alias sub_title subtitle
|
87
|
+
|
88
|
+
##
|
89
|
+
# Set or overwrite the body (content).
|
90
|
+
#
|
91
|
+
# The message to display in the notification.
|
92
|
+
#
|
93
|
+
def body(value)
|
94
|
+
_params[:body] = value.nil? ? nil : String(value)
|
95
|
+
self
|
96
|
+
rescue NoMethodError
|
97
|
+
raise ArgumentError, 'body must be nil or string-like'
|
98
|
+
end
|
99
|
+
|
100
|
+
alias content body
|
101
|
+
|
102
|
+
##
|
103
|
+
# Set or overwrite the sound.
|
104
|
+
#
|
105
|
+
# Play a sound when the recipient receives this notification. Specify
|
106
|
+
# "default" to play the device's default notification sound, or nil to
|
107
|
+
# play no sound. Custom sounds are not supported.
|
108
|
+
#
|
109
|
+
# @note iOS only
|
110
|
+
#
|
111
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
112
|
+
def sound(value)
|
113
|
+
if value.nil?
|
114
|
+
_params[:sound] = nil
|
115
|
+
return self
|
116
|
+
end
|
117
|
+
|
118
|
+
unless value.respond_to?(:to_h)
|
119
|
+
_params[:sound] = String(value)
|
120
|
+
return self
|
121
|
+
end
|
122
|
+
|
123
|
+
json_value = value.to_h
|
124
|
+
|
125
|
+
next_value = {
|
126
|
+
critical: !json_value.fetch(:critical, nil).nil?,
|
127
|
+
name: json_value.fetch(:name, nil),
|
128
|
+
volume: json_value.fetch(:volume, nil)
|
129
|
+
}
|
130
|
+
|
131
|
+
next_value[:name] = String(next_value[:name]) unless next_value[:name].nil?
|
132
|
+
next_value[:volume] = next_value[:volume].to_i unless next_value[:volume].nil?
|
133
|
+
|
134
|
+
_params[:sound] = next_value.compact
|
135
|
+
|
136
|
+
self
|
137
|
+
end
|
138
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
139
|
+
|
140
|
+
##
|
141
|
+
# Set or overwrite the time to live in seconds.
|
142
|
+
#
|
143
|
+
# The number of seconds for which the message may be kept around for
|
144
|
+
# redelivery if it hasn't been delivered yet. Defaults to nil in order to
|
145
|
+
# use the respective defaults of each provider:
|
146
|
+
#
|
147
|
+
# - 0 for iOS/APNs
|
148
|
+
# - 2419200 (4 weeks) for Android/FCM
|
149
|
+
#
|
150
|
+
# @see expiration
|
151
|
+
#
|
152
|
+
# @note On Android, we make a best effort to deliver messages with zero
|
153
|
+
# TTL immediately and do not throttle them. However, setting TTL to a
|
154
|
+
# low value (e.g. zero) can prevent normal-priority notifications from
|
155
|
+
# ever reaching Android devices that are in doze mode. In order to
|
156
|
+
# guarantee that a notification will be delivered, TTL must be long
|
157
|
+
# enough for the device to wake from doze mode.
|
158
|
+
#
|
159
|
+
def ttl(value)
|
160
|
+
_params[:ttl] = value.nil? ? nil : value.to_i
|
161
|
+
self
|
162
|
+
rescue NoMethodError
|
163
|
+
raise ArgumentError, 'ttl must be numeric or nil'
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Set or overwrite the time to live based on a unix timestamp.
|
168
|
+
#
|
169
|
+
# Timestamp since the UNIX epoch specifying when the message expires.
|
170
|
+
# Same effect as ttl (ttl takes precedence over expiration).
|
171
|
+
#
|
172
|
+
# @see ttl
|
173
|
+
#
|
174
|
+
def expiration(value)
|
175
|
+
_params[:expiration] = value.nil? ? nil : value.to_i
|
176
|
+
self
|
177
|
+
rescue NoMethodError
|
178
|
+
raise ArgumentError, 'ttl must be numeric or nil'
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Set or overwrite the priority.
|
183
|
+
#
|
184
|
+
# The delivery priority of the message. Specify "default" or nil to use
|
185
|
+
# the default priority on each platform:
|
186
|
+
#
|
187
|
+
# - "normal" on Android
|
188
|
+
# - "high" on iOS
|
189
|
+
#
|
190
|
+
# @note On Android, normal-priority messages won't open network
|
191
|
+
# connections on sleeping devices and their delivery may be delayed to
|
192
|
+
# conserve the battery. High-priority messages are delivered
|
193
|
+
# immediately if possible and may wake sleeping devices to open network
|
194
|
+
# connections, consuming energy.
|
195
|
+
#
|
196
|
+
# @note On iOS, normal-priority messages are sent at a time that takes
|
197
|
+
# into account power considerations for the device, and may be grouped
|
198
|
+
# and delivered in bursts. They are throttled and may not be delivered
|
199
|
+
# by Apple. High-priority messages are sent immediately.
|
200
|
+
# Normal priority corresponds to APNs priority level 5 and high
|
201
|
+
# priority to 10.
|
202
|
+
#
|
203
|
+
# rubocop:disable Metrics/MethodLength
|
204
|
+
def priority(value)
|
205
|
+
if value.nil?
|
206
|
+
_params[:priority] = nil
|
207
|
+
return self
|
208
|
+
end
|
209
|
+
|
210
|
+
priority_string = String(value)
|
211
|
+
|
212
|
+
unless %w[default normal high].include?(priority_string)
|
213
|
+
raise ArgumentError, 'priority must be default, normal, or high'
|
214
|
+
end
|
215
|
+
|
216
|
+
_params[:priority] = priority_string
|
217
|
+
self
|
218
|
+
rescue NoMethodError
|
219
|
+
raise ArgumentError, 'priority must be default, normal, or high'
|
220
|
+
end
|
221
|
+
# rubocop:enable Metrics/MethodLength
|
222
|
+
|
223
|
+
##
|
224
|
+
# Set or overwrite the new badge count.
|
225
|
+
#
|
226
|
+
# Use 0 to clear, use nil to keep as is.
|
227
|
+
#
|
228
|
+
# @note iOS only
|
229
|
+
#
|
230
|
+
def badge(value)
|
231
|
+
_params[:badge] = value.nil? ? nil : value.to_i
|
232
|
+
self
|
233
|
+
rescue NoMethodError
|
234
|
+
raise ArgumentError, 'badge must be numeric or nil'
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# Set or overwrite the channel ID.
|
239
|
+
#
|
240
|
+
# ID of the Notification Channel through which to display this
|
241
|
+
# notification. If an ID is specified but the corresponding channel does
|
242
|
+
# not exist on the device (i.e. has not yet been created by your app),
|
243
|
+
# the notification will not be displayed to the user.
|
244
|
+
#
|
245
|
+
# @note If left nil, a "Default" channel will be used, and Expo will
|
246
|
+
# create the channel on the device if it does not yet exist. However,
|
247
|
+
# use caution, as the "Default" channel is user-facing and you may not
|
248
|
+
# be able to fully delete it.
|
249
|
+
#
|
250
|
+
# @note Android only
|
251
|
+
#
|
252
|
+
def channel_id(value)
|
253
|
+
_params[:channelId] = value.nil? ? nil : String(value)
|
254
|
+
self
|
255
|
+
rescue NoMethodError
|
256
|
+
raise ArgumentError, 'channelId must be string-like or nil to use "Default"'
|
257
|
+
end
|
258
|
+
|
259
|
+
alias channel_identifier channel_id
|
260
|
+
|
261
|
+
##
|
262
|
+
# Set or overwrite the category ID
|
263
|
+
#
|
264
|
+
# ID of the notification category that this notification is associated
|
265
|
+
# with. Must be on at least SDK 41 or bare workflow.
|
266
|
+
#
|
267
|
+
# Notification categories allow you to create interactive push
|
268
|
+
# notifications, so that a user can respond directly to the incoming
|
269
|
+
# notification either via buttons or a text response. A category defines
|
270
|
+
# the set of actions a user can take, and then those actions are applied
|
271
|
+
# to a notification by specifying the categoryId here.
|
272
|
+
#
|
273
|
+
# @see https://docs.expo.dev/versions/latest/sdk/notifications/#managing-notification-categories-interactive-notifications
|
274
|
+
#
|
275
|
+
def category_id(value)
|
276
|
+
_params[:categoryId] = value.nil? ? nil : String(value)
|
277
|
+
self
|
278
|
+
rescue NoMethodError
|
279
|
+
raise ArgumentError, 'categoryId must be string-like or nil'
|
280
|
+
end
|
281
|
+
|
282
|
+
##
|
283
|
+
# Set or overwrite the mutability flag.
|
284
|
+
#
|
285
|
+
# Use nil to use the defaults.
|
286
|
+
#
|
287
|
+
# Specifies whether this notification can be intercepted by the client
|
288
|
+
# app. In Expo Go, this defaults to true, and if you change that to
|
289
|
+
# false, you may experience issues. In standalone and bare apps, this
|
290
|
+
# defaults to false.
|
291
|
+
#
|
292
|
+
def mutable_content(value)
|
293
|
+
_params[:mutableContent] = value.nil? ? nil : !value.nil?
|
294
|
+
self
|
295
|
+
end
|
296
|
+
|
297
|
+
alias mutable mutable_content
|
298
|
+
|
299
|
+
##
|
300
|
+
# Add a single recipient
|
301
|
+
#
|
302
|
+
# Must be a valid Expo Push Token, or a PushTokenInvalid error is raised.
|
303
|
+
#
|
304
|
+
# @see PushTokenInvalid
|
305
|
+
# @see #to
|
306
|
+
#
|
307
|
+
def <<(recipient)
|
308
|
+
raise PushTokenInvalid.new(token: recipient) unless Expo::Push.expo_push_token?(recipient)
|
309
|
+
|
310
|
+
recipients << recipient
|
311
|
+
|
312
|
+
self
|
313
|
+
end
|
314
|
+
|
315
|
+
alias add_recipient <<
|
316
|
+
alias add_recipients to
|
317
|
+
|
318
|
+
##
|
319
|
+
# Allows overwriting the recipients list which is necessary to prepare
|
320
|
+
# the notification when chunking.
|
321
|
+
#
|
322
|
+
def prepare(targets)
|
323
|
+
dup.tap do |prepared|
|
324
|
+
prepared.reset_recipients(targets)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def count
|
329
|
+
recipients.length
|
330
|
+
end
|
331
|
+
|
332
|
+
def as_json
|
333
|
+
puts _params
|
334
|
+
|
335
|
+
{ to: recipients }.merge(_params.compact)
|
336
|
+
end
|
337
|
+
|
338
|
+
def reset_recipients(targets)
|
339
|
+
self.recipients = []
|
340
|
+
add_recipients(targets)
|
341
|
+
end
|
342
|
+
|
343
|
+
private
|
344
|
+
|
345
|
+
attr_accessor :_params
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|