expo-server-sdk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|