action_push_native 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/MIT-LICENSE +20 -0
- data/README.md +329 -0
- data/Rakefile +8 -0
- data/app/jobs/action_push_native/notification_job.rb +58 -0
- data/app/models/action_push_native/device.rb +20 -0
- data/db/migrate/20250610075650_create_action_push_native_device.rb +12 -0
- data/lib/action_push_native/configured_notification.rb +35 -0
- data/lib/action_push_native/engine.rb +11 -0
- data/lib/action_push_native/errors.rb +17 -0
- data/lib/action_push_native/notification.rb +87 -0
- data/lib/action_push_native/service/apns.rb +125 -0
- data/lib/action_push_native/service/fcm.rb +124 -0
- data/lib/action_push_native/version.rb +3 -0
- data/lib/action_push_native.rb +40 -0
- data/lib/generators/action_push_native/install/install_generator.rb +12 -0
- data/lib/generators/action_push_native/install/templates/app/jobs/application_push_notification_job.rb.tt +7 -0
- data/lib/generators/action_push_native/install/templates/app/models/application_push_device.rb.tt +4 -0
- data/lib/generators/action_push_native/install/templates/app/models/application_push_notification.rb.tt +12 -0
- data/lib/generators/action_push_native/install/templates/config/push.yml.tt +34 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c37771ec586061a95004904931f0ffc841177a104fa27b8f5c9404bdef33bcc8
|
4
|
+
data.tar.gz: d39db792468a2516ce99d9330f0612b1ba1ef1ba194ee0cfa3b4f493164c214e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 761d8d0a9eeb0f4914fa2129dda438958babf02354be17ed87b8264ebd052d10ff73a98723afbeb40c6dc0253de81f26ed37fc93588b3315b08264181cee1554
|
7
|
+
data.tar.gz: b0351f879b5fcf6a244c053bcd375ea4803071ec82d8d56329bd22e4eb7a346f8219f56b34497c27faf705a094490164399df7526dbf04daf89b8d73344d9330
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 37signals, LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,329 @@
|
|
1
|
+
# Action Push Native
|
2
|
+
|
3
|
+
Action Push Native is a Rails push notification gem for mobile platforms, supporting APNs (Apple) and FCM (Google).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
1. bundle add action_push_native
|
9
|
+
2. bin/rails g action_push_native:install
|
10
|
+
3. bin/rails action_push_native:install:migrations
|
11
|
+
4. bin/rails db:migrate
|
12
|
+
```
|
13
|
+
|
14
|
+
This will install the gem and run the necessary migrations to set up the database.
|
15
|
+
|
16
|
+
## Configuration
|
17
|
+
|
18
|
+
The installation will create:
|
19
|
+
|
20
|
+
- `app/models/application_push_notification.rb`
|
21
|
+
- `app/jobs/application_push_notification_job.rb`
|
22
|
+
- `app/models/application_push_device.rb`
|
23
|
+
- `config/push.yml`
|
24
|
+
|
25
|
+
`app/models/application_push_notification.rb`:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class ApplicationPushNotification < ActionPushNative::Notification
|
29
|
+
# Set a custom job queue_name
|
30
|
+
queue_as :realtime
|
31
|
+
|
32
|
+
# Controls whether push notifications are enabled (default: !Rails.env.test?)
|
33
|
+
self.enabled = Rails.env.production?
|
34
|
+
|
35
|
+
# Define a custom callback to modify or abort the notification before it is sent
|
36
|
+
before_delivery do |notification|
|
37
|
+
throw :abort if Notification.find(notification.context[:notification_id]).expired?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Used to create and send push notifications. You can customize it by subclassing or
|
43
|
+
you can change the application defaults by editing it directly.
|
44
|
+
|
45
|
+
`app/jobs/application_push_notification_job.rb`:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class ApplicationPushNotificationJob < ActionPushNative::NotificationJob
|
49
|
+
# Enable logging job arguments (default: false)
|
50
|
+
self.log_arguments = true
|
51
|
+
|
52
|
+
# Report job retries via the `Rails.error` reporter (default: false)
|
53
|
+
self.report_job_retries = true
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
Job class that processes the push notifications. You can customize it by editing it
|
58
|
+
directly in your application.
|
59
|
+
|
60
|
+
`app/models/application_push_device.rb`:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class ApplicationPushDevice < ActionPushNative::Device
|
64
|
+
# Customize TokenError handling (default: destroy!)
|
65
|
+
# rescue_from (ActionPushNative::TokenError) { Rails.logger.error("Device #{id} token is invalid") }
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
This represents a push notification device. You can customize it by editing it directly in your application.
|
70
|
+
|
71
|
+
`config/push.yml`:
|
72
|
+
|
73
|
+
```yaml
|
74
|
+
shared:
|
75
|
+
apple:
|
76
|
+
# Token auth params
|
77
|
+
# See https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns
|
78
|
+
key_id: your_key_id
|
79
|
+
encryption_key: your_apple_encryption_key
|
80
|
+
|
81
|
+
team_id: your_apple_team_id
|
82
|
+
# Your identifier found on https://developer.apple.com/account/resources/identifiers/list
|
83
|
+
topic: your.bundle.identifier
|
84
|
+
|
85
|
+
google:
|
86
|
+
# Your Firebase project service account credentials
|
87
|
+
# See https://firebase.google.com/docs/cloud-messaging/auth-server
|
88
|
+
encryption_key: your_service_account_json_file
|
89
|
+
|
90
|
+
# Firebase project_id
|
91
|
+
project_id: your_project_id
|
92
|
+
```
|
93
|
+
|
94
|
+
This file contains the configuration for the push notification services you want to use.
|
95
|
+
The push notification services supported are `apple` (APNs) and `google` (FCM).
|
96
|
+
If you're configuring more than one app, see the section [Configuring multiple apps](#configuring-multiple-apps) below.
|
97
|
+
|
98
|
+
### Configuring multiple apps
|
99
|
+
|
100
|
+
You can send push notifications to multiple apps using different notification classes.
|
101
|
+
Each notification class need to inherit from `ApplicationPushNotification` and set `self.application`, to a key set in `push.yml`
|
102
|
+
for each supported platform. You can also (optionally) set a shared `application` option in `push.yml`.
|
103
|
+
This acts as the base configuration for that platform, and its values will be merged (and overridden) with the matching app-specific configuration.
|
104
|
+
|
105
|
+
In the example below we are configuring two apps: `calendar` and `email` using respectively the
|
106
|
+
`CalendarPushNotification` and `EmailPushNotification` notification classes.
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class CalendarPushNotification < ApplicationPushNotification
|
110
|
+
self.application = "calendar"
|
111
|
+
|
112
|
+
# Custom notification logic for calendar app
|
113
|
+
end
|
114
|
+
|
115
|
+
class EmailPushNotification < ApplicationPushNotification
|
116
|
+
self.application = "email"
|
117
|
+
|
118
|
+
# Custom notification logic for email app
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
```yaml
|
123
|
+
shared:
|
124
|
+
apple:
|
125
|
+
# Base configuration for Apple platform
|
126
|
+
# This will be merged with the app-specific configuration
|
127
|
+
application:
|
128
|
+
team_id: your_apple_team_id
|
129
|
+
|
130
|
+
calendar:
|
131
|
+
# Token auth params
|
132
|
+
# See https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns
|
133
|
+
key_id: calendar_key_id
|
134
|
+
encryption_key: calendar_apple_encryption_key
|
135
|
+
# Your identifier found on https://developer.apple.com/account/resources/identifiers/list
|
136
|
+
topic: calendar.bundle.identifier
|
137
|
+
|
138
|
+
email:
|
139
|
+
# Token auth params
|
140
|
+
# See https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns
|
141
|
+
key_id: email_key_id
|
142
|
+
encryption_key: email_apple_encryption_key
|
143
|
+
# Your identifier found on https://developer.apple.com/account/resources/identifiers/list
|
144
|
+
topic: email.bundle.identifier
|
145
|
+
|
146
|
+
google:
|
147
|
+
calendar:
|
148
|
+
# Your Firebase project service account credentials
|
149
|
+
# See https://firebase.google.com/docs/cloud-messaging/auth-server
|
150
|
+
encryption_key: calendar_service_account_json_file
|
151
|
+
|
152
|
+
# Firebase project_id
|
153
|
+
project_id: calendar_project_id
|
154
|
+
|
155
|
+
email:
|
156
|
+
# Your Firebase project service account credentials
|
157
|
+
# See https://firebase.google.com/docs/cloud-messaging/auth-server
|
158
|
+
encryption_key: email_service_account_json_file
|
159
|
+
|
160
|
+
# Firebase project_id
|
161
|
+
project_id: email_project_id
|
162
|
+
```
|
163
|
+
|
164
|
+
## Usage
|
165
|
+
|
166
|
+
### Create and send a notification asynchronously to a device
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
device = ApplicationPushDevice.create! \
|
170
|
+
name: "iPhone 16",
|
171
|
+
token: "6c267f26b173cd9595ae2f6702b1ab560371a60e7c8a9e27419bd0fa4a42e58f",
|
172
|
+
platform: "apple"
|
173
|
+
|
174
|
+
notification = ApplicationPushNotification.new \
|
175
|
+
title: "Hello world!",
|
176
|
+
body: "Welcome to Action Push Native"
|
177
|
+
|
178
|
+
notification.deliver_later_to(device)
|
179
|
+
```
|
180
|
+
|
181
|
+
`deliver_later_to` supports also an array of devices:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
notification.deliver_later_to([ device1, device2 ])
|
185
|
+
```
|
186
|
+
|
187
|
+
A notification can also be delivered synchronously using `deliver_to`:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
notification.deliver_to(device)
|
191
|
+
```
|
192
|
+
|
193
|
+
It is recommended to send notifications asynchronously using `deliver_later_to`.
|
194
|
+
This ensures error handling and retry logic are in place, and avoids blocking your application's execution.
|
195
|
+
|
196
|
+
### Application data attributes
|
197
|
+
|
198
|
+
You can pass custom data to the application using the `with_data` method:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
notification = ApplicationPushNotification
|
202
|
+
.with_data({ badge: "1" })
|
203
|
+
.new(title: "Welcome to Action Push Native")
|
204
|
+
```
|
205
|
+
|
206
|
+
### Custom platform Payload
|
207
|
+
|
208
|
+
You can configure custom platform payload to be sent with the notification. This is useful when you
|
209
|
+
need to send additional data that is specific to the platform you are using.
|
210
|
+
|
211
|
+
You can use `with_apple` for Apple and `with_google` for Google:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
notification = ApplicationPushNotification
|
215
|
+
.with_apple(category: "observable")
|
216
|
+
.with_google(data: { badge: 1 })
|
217
|
+
.new(title: "Hello world!")
|
218
|
+
```
|
219
|
+
|
220
|
+
The platform payload takes precedence over the other fields, and you can use it to override the
|
221
|
+
default behaviour:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
notification = ApplicationPushNotification
|
225
|
+
.with_google(android: { notification: { notification_count: nil } })
|
226
|
+
.new(title: "Hello world!", body: "Welcome to Action Push Native", badge: 1)
|
227
|
+
```
|
228
|
+
|
229
|
+
This will unset the default `notification_count` (`badge`) field in the Google payload, while keeping `title`
|
230
|
+
and `body`.
|
231
|
+
|
232
|
+
### Silent Notifications
|
233
|
+
|
234
|
+
You can create a silent notification via the `silent` method:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
notification = ApplicationPushNotification.silent.with_data(id: 1)
|
238
|
+
```
|
239
|
+
|
240
|
+
This will create a silent notification for both Apple and Google platforms and sets an application
|
241
|
+
data field of `{ id: 1 }` for both platforms. Silent push notification must not contain any attribute which would trigger
|
242
|
+
a visual notification on the device, such as `title`, `body`, `badge`, etc.
|
243
|
+
|
244
|
+
### Linking a Device to a Record
|
245
|
+
|
246
|
+
A Device can be associated with any record in your application via the `owner` polymorphic association:
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
user = User.find_by_email_address("jacopo@37signals.com")
|
250
|
+
|
251
|
+
ApplicationPushDevice.create! \
|
252
|
+
name: "iPhone 16",
|
253
|
+
token: "6c267f26b173cd9595ae2f6702b1ab560371a60e7c8a9e27419bd0fa4a42e58f",
|
254
|
+
platform: "apple",
|
255
|
+
owner: user
|
256
|
+
```
|
257
|
+
### `before_delivery` callback
|
258
|
+
|
259
|
+
You can specify Active Record like callbacks for the `delivery` method. For example, you can modify
|
260
|
+
or cancel the notification by specifying a custom `before_delivery` block. The callback has access
|
261
|
+
to the `notification` object. You can also pass additional context data to the notification
|
262
|
+
by adding extra arguments to the notification constructor:
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
class CalendarPushNotification < ApplicationPushNotification
|
266
|
+
before_delivery do |notification|
|
267
|
+
throw :abort if Calendar.find(notification.context[:calendar_id]).expired?
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
data = { calendar_id: @calendar.id, identity_id: @identity.id }
|
272
|
+
|
273
|
+
notification = CalendarPushNotification
|
274
|
+
.with_apple(custom_payload: data)
|
275
|
+
.with_google(data: data)
|
276
|
+
.new(calendar_id: 123)
|
277
|
+
|
278
|
+
notification.deliver_later_to(device)
|
279
|
+
```
|
280
|
+
|
281
|
+
### Using a custom Device model
|
282
|
+
|
283
|
+
If using the default `ApplicationPushDevice` model does not fit your needs, you can create a custom
|
284
|
+
device model, as long as:
|
285
|
+
|
286
|
+
1. It can be serialized and deserialized by `ActiveJob`.
|
287
|
+
2. It responds to the `token` and `platform` methods.
|
288
|
+
3. It implements a `push` method like this:
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
class CustomDevice
|
292
|
+
# Your custom device attributes and methods...
|
293
|
+
|
294
|
+
def push(notification)
|
295
|
+
notification.token = token
|
296
|
+
ActionPushNative.service_for(platform, notification).push(notification)
|
297
|
+
rescue ActionPushNative::TokenError => error
|
298
|
+
# Custom token error handling
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
## `ActionPushNative::Notification` attributes
|
304
|
+
|
305
|
+
| Name | Description
|
306
|
+
|------------------|------------
|
307
|
+
| :title | The title of the notification.
|
308
|
+
| :body | The body of the notification.
|
309
|
+
| :badge | The badge number to display on the app icon.
|
310
|
+
| :thread_id | The thread identifier for grouping notifications.
|
311
|
+
| :sound | The sound to play when the notification is received.
|
312
|
+
| :high_priority | Whether the notification should be sent with high priority (default: true).
|
313
|
+
| :google_data | The Google-specific payload for the notification.
|
314
|
+
| :apple_data | The Apple-specific payload for the notification.
|
315
|
+
| :data | The data payload for the notification, sent to all platforms.
|
316
|
+
| ** | Any additional attributes passed to the constructor will be merged in the `context` hash.
|
317
|
+
|
318
|
+
### Factory methods
|
319
|
+
|
320
|
+
| Name | Description
|
321
|
+
|------------------|------------
|
322
|
+
| :with_apple | Set the Apple-specific payload for the notification.
|
323
|
+
| :with_google | Set the Google-specific payload for the notification.
|
324
|
+
| :with_data | Set the data payload for the notification, sent to all platforms.
|
325
|
+
| :silent | Create a silent notification that does not trigger a visual alert on the device.
|
326
|
+
|
327
|
+
## License
|
328
|
+
|
329
|
+
Action Push Native is licensed under MIT.
|
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPushNative
|
4
|
+
class NotificationJob < ActiveJob::Base
|
5
|
+
self.log_arguments = false
|
6
|
+
|
7
|
+
class_attribute :report_job_retries, default: false
|
8
|
+
|
9
|
+
discard_on ActiveJob::DeserializationError
|
10
|
+
discard_on BadDeviceTopicError do |_job, error|
|
11
|
+
Rails.error.report(error)
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def retry_options
|
16
|
+
Rails.version >= "8.1" ? { report: report_job_retries } : {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Exponential backoff starting from a minimum of 1 minute, capped at 60m as suggested by FCM:
|
20
|
+
# https://firebase.google.com/docs/cloud-messaging/scale-fcm#errors
|
21
|
+
#
|
22
|
+
# | Executions | Delay (rounded minutes) |
|
23
|
+
# |------------|-------------------------|
|
24
|
+
# | 1 | 1 |
|
25
|
+
# | 2 | 2 |
|
26
|
+
# | 3 | 4 |
|
27
|
+
# | 4 | 8 |
|
28
|
+
# | 5 | 16 |
|
29
|
+
# | 6 | 32 |
|
30
|
+
# | 7 | 60 (cap) |
|
31
|
+
def exponential_backoff_delay(executions)
|
32
|
+
base_wait = 1.minute
|
33
|
+
delay = base_wait * (2**(executions - 1))
|
34
|
+
jitter = 0.15
|
35
|
+
jitter_delay = rand * delay * jitter
|
36
|
+
|
37
|
+
[ delay + jitter_delay, 60.minutes ].min
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
with_options retry_options do
|
42
|
+
retry_on TimeoutError, wait: 1.minute
|
43
|
+
retry_on ConnectionError, ConnectionPool::TimeoutError, attempts: 20
|
44
|
+
|
45
|
+
# Altough unexpected, these are short-lived errors that can be retried most of the times.
|
46
|
+
retry_on ForbiddenError, BadRequestError
|
47
|
+
end
|
48
|
+
|
49
|
+
with_options wait: ->(executions) { exponential_backoff_delay(executions) }, attempts: 6, **retry_options do
|
50
|
+
retry_on TooManyRequestsError, ServiceUnavailableError, InternalServerError
|
51
|
+
retry_on Signet::RemoteServerError
|
52
|
+
end
|
53
|
+
|
54
|
+
def perform(notification_class, notification_attributes, device)
|
55
|
+
notification_class.constantize.new(**notification_attributes).deliver_to(device)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPushNative
|
4
|
+
class Device < ApplicationRecord
|
5
|
+
include ActiveSupport::Rescuable
|
6
|
+
|
7
|
+
rescue_from(TokenError) { destroy! }
|
8
|
+
|
9
|
+
belongs_to :owner, polymorphic: true, optional: true
|
10
|
+
|
11
|
+
enum :platform, { apple: "apple", google: "google" }
|
12
|
+
|
13
|
+
def push(notification)
|
14
|
+
notification.token = token
|
15
|
+
ActionPushNative.service_for(platform, notification).push(notification)
|
16
|
+
rescue => error
|
17
|
+
rescue_with_handler(error) || raise
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateActionPushNativeDevice < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :action_push_native_devices do |t|
|
4
|
+
t.string :name
|
5
|
+
t.string :platform, null: false
|
6
|
+
t.string :token, null: false
|
7
|
+
t.belongs_to :owner, polymorphic: true
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActionPushNative
|
2
|
+
class ConfiguredNotification
|
3
|
+
def initialize(notification_class)
|
4
|
+
@notification_class = notification_class
|
5
|
+
@options = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def new(**attributes)
|
9
|
+
notification_class.new(**attributes.merge(options))
|
10
|
+
end
|
11
|
+
|
12
|
+
def with_data(data)
|
13
|
+
@options[:data] = @options.fetch(:data, {}).merge(data)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def silent
|
18
|
+
@options = options.merge(high_priority: false)
|
19
|
+
with_apple(content_available: 1)
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_apple(apple_data)
|
23
|
+
@options[:apple_data] = @options.fetch(:apple_data, {}).merge(apple_data)
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def with_google(google_data)
|
28
|
+
@options[:google_data] = @options.fetch(:google_data, {}).merge(google_data)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
attr_reader :notification_class, :options
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPushNative
|
4
|
+
class TimeoutError < StandardError; end
|
5
|
+
class ConnectionError < StandardError; end
|
6
|
+
|
7
|
+
class BadRequestError < StandardError; end
|
8
|
+
class ForbiddenError < StandardError; end
|
9
|
+
class PayloadTooLargeError < StandardError; end
|
10
|
+
class TooManyRequestsError < StandardError; end
|
11
|
+
class ServiceUnavailableError < StandardError; end
|
12
|
+
class InternalServerError < StandardError; end
|
13
|
+
class BadDeviceTopicError < StandardError; end
|
14
|
+
class NotFoundError < StandardError; end
|
15
|
+
|
16
|
+
class TokenError < StandardError; end
|
17
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPushNative
|
4
|
+
# = Action Push Native Notification
|
5
|
+
#
|
6
|
+
# A notification that can be delivered to devices.
|
7
|
+
class Notification
|
8
|
+
extend ActiveModel::Callbacks
|
9
|
+
|
10
|
+
attr_accessor :title, :body, :badge, :thread_id, :sound, :high_priority, :apple_data, :google_data, :data
|
11
|
+
attr_accessor :context
|
12
|
+
attr_accessor :token
|
13
|
+
|
14
|
+
define_model_callbacks :delivery
|
15
|
+
|
16
|
+
class_attribute :queue_name, default: ActiveJob::Base.default_queue_name
|
17
|
+
class_attribute :enabled, default: !Rails.env.test?
|
18
|
+
class_attribute :application
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def queue_as(name)
|
22
|
+
self.queue_name = name
|
23
|
+
end
|
24
|
+
|
25
|
+
delegate :with_data, :silent, :with_apple, :with_google, to: :configured_notification
|
26
|
+
|
27
|
+
private
|
28
|
+
def configured_notification
|
29
|
+
ConfiguredNotification.new(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# === Attributes
|
34
|
+
#
|
35
|
+
# title - The title
|
36
|
+
# body - The message body
|
37
|
+
# badge - The badge number to display on the app icon
|
38
|
+
# thread_id - The thread ID for grouping notifications
|
39
|
+
# sound - The sound to play when the notification is received
|
40
|
+
# high_priority - Whether to send the notification with high priority (default: true).
|
41
|
+
# For silent notifications is recommended to set this to false
|
42
|
+
# apple_data - Apple Push Notification Service (APNS) specific data
|
43
|
+
# google_data - Firebase Cloud Messaging (FCM) specific data
|
44
|
+
# data - Custom data to be sent with the notification
|
45
|
+
#
|
46
|
+
# Any extra attributes are set inside the `context` hash.
|
47
|
+
def initialize(title: nil, body: nil, badge: nil, thread_id: nil, sound: nil, high_priority: true, apple_data: {}, google_data: {}, data: {}, **context)
|
48
|
+
@title = title
|
49
|
+
@body = body
|
50
|
+
@badge = badge
|
51
|
+
@thread_id = thread_id
|
52
|
+
@sound = sound
|
53
|
+
@high_priority = high_priority
|
54
|
+
@apple_data = apple_data
|
55
|
+
@google_data = google_data
|
56
|
+
@data = data
|
57
|
+
@context = context
|
58
|
+
end
|
59
|
+
|
60
|
+
def deliver_to(device)
|
61
|
+
if enabled
|
62
|
+
run_callbacks(:delivery) { device.push(self) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def deliver_later_to(devices)
|
67
|
+
Array(devices).each do |device|
|
68
|
+
ApplicationPushNotificationJob.set(queue: queue_name).perform_later(self.class.name, self.as_json, device)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def as_json
|
73
|
+
{
|
74
|
+
title: title,
|
75
|
+
body: body,
|
76
|
+
badge: badge,
|
77
|
+
thread_id: thread_id,
|
78
|
+
sound: sound,
|
79
|
+
high_priority: high_priority,
|
80
|
+
apple_data: apple_data,
|
81
|
+
google_data: google_data,
|
82
|
+
data: data,
|
83
|
+
**context
|
84
|
+
}.compact
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPushNative
|
4
|
+
module Service
|
5
|
+
class Apns
|
6
|
+
DEFAULT_TIMEOUT = 30.seconds
|
7
|
+
DEFAULT_POOL_SIZE = 5
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
# Per-application connection pools
|
14
|
+
cattr_accessor :connection_pools
|
15
|
+
|
16
|
+
def push(notification)
|
17
|
+
reset_connection_error
|
18
|
+
|
19
|
+
connection_pool.with do |connection|
|
20
|
+
rescue_and_reraise_network_errors do
|
21
|
+
apnotic_notification = apnotic_notification_from(notification)
|
22
|
+
Rails.logger.info("Pushing APNs notification: #{apnotic_notification.apns_id}")
|
23
|
+
|
24
|
+
response = connection.push \
|
25
|
+
apnotic_notification,
|
26
|
+
timeout: config[:request_timeout] || DEFAULT_TIMEOUT
|
27
|
+
raise connection_error if connection_error
|
28
|
+
handle_response_error(response) unless response&.ok?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
attr_reader :config, :connection_error
|
35
|
+
|
36
|
+
def reset_connection_error
|
37
|
+
@connection_error = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def connection_pool
|
41
|
+
self.class.connection_pools ||= {}
|
42
|
+
self.class.connection_pools[config] ||= build_connection_pool
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_connection_pool
|
46
|
+
build_method = config[:connect_to_development_server] ? "development" : "new"
|
47
|
+
Apnotic::ConnectionPool.public_send(build_method, {
|
48
|
+
auth_method: :token,
|
49
|
+
cert_path: StringIO.new(config.fetch(:encryption_key)),
|
50
|
+
key_id: config.fetch(:key_id),
|
51
|
+
team_id: config.fetch(:team_id)
|
52
|
+
}, size: config[:connection_pool_size] || DEFAULT_POOL_SIZE) do |connection|
|
53
|
+
# Prevents the main thread from crashing collecting the connection error from the off-thread
|
54
|
+
# and raising it afterwards.
|
55
|
+
connection.on(:error) { |error| @connection_error = error }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def rescue_and_reraise_network_errors
|
60
|
+
begin
|
61
|
+
yield
|
62
|
+
rescue Errno::ETIMEDOUT => e
|
63
|
+
raise ActionPushNative::TimeoutError, e.message
|
64
|
+
rescue Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
|
65
|
+
raise ActionPushNative::ConnectionError, e.message
|
66
|
+
rescue OpenSSL::SSL::SSLError => e
|
67
|
+
if e.message.include?("SSL_connect")
|
68
|
+
raise ActionPushNative::ConnectionError, e.message
|
69
|
+
else
|
70
|
+
raise
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
PRIORITIES = { high: 10, normal: 5 }.freeze
|
76
|
+
|
77
|
+
def apnotic_notification_from(notification)
|
78
|
+
Apnotic::Notification.new(notification.token).tap do |n|
|
79
|
+
n.topic = config.fetch(:topic)
|
80
|
+
n.alert = { title: notification.title, body: notification.body }.compact
|
81
|
+
n.badge = notification.badge
|
82
|
+
n.thread_id = notification.thread_id
|
83
|
+
n.sound = notification.sound
|
84
|
+
n.priority = notification.high_priority ? PRIORITIES[:high] : PRIORITIES[:normal]
|
85
|
+
n.custom_payload = notification.data
|
86
|
+
notification.apple_data&.each do |key, value|
|
87
|
+
n.public_send("#{key.to_s.underscore}=", value)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_response_error(response)
|
93
|
+
code = response&.status
|
94
|
+
reason = response.body["reason"] if response
|
95
|
+
|
96
|
+
Rails.logger.error("APNs response error #{code}: #{reason}") if reason
|
97
|
+
|
98
|
+
case [ code, reason ]
|
99
|
+
in [ nil, _ ]
|
100
|
+
raise ActionPushNative::TimeoutError
|
101
|
+
in [ "400", "BadDeviceToken" ]
|
102
|
+
raise ActionPushNative::TokenError, reason
|
103
|
+
in [ "400", "DeviceTokenNotForTopic" ]
|
104
|
+
raise ActionPushNative::BadDeviceTopicError, reason
|
105
|
+
in [ "400", _ ]
|
106
|
+
raise ActionPushNative::BadRequestError, reason
|
107
|
+
in [ "403", _ ]
|
108
|
+
raise ActionPushNative::ForbiddenError, reason
|
109
|
+
in [ "404", _ ]
|
110
|
+
raise ActionPushNative::NotFoundError, reason
|
111
|
+
in [ "410", _ ]
|
112
|
+
raise ActionPushNative::TokenError, reason
|
113
|
+
in [ "413", _ ]
|
114
|
+
raise ActionPushNative::PayloadTooLargeError, reason
|
115
|
+
in [ "429", _ ]
|
116
|
+
raise ActionPushNative::TooManyRequestsError, reason
|
117
|
+
in [ "503", _ ]
|
118
|
+
raise ActionPushNative::ServiceUnavailableError, reason
|
119
|
+
else
|
120
|
+
raise ActionPushNative::InternalServerError, reason
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionPushNative
|
4
|
+
module Service
|
5
|
+
class Fcm
|
6
|
+
# FCM suggests at least a 10s timeout for requests, we set 15 to add some buffer.
|
7
|
+
# https://firebase.google.com/docs/cloud-messaging/scale-fcm#timeouts
|
8
|
+
DEFAULT_TIMEOUT = 15.seconds
|
9
|
+
|
10
|
+
def initialize(config)
|
11
|
+
@config = config
|
12
|
+
end
|
13
|
+
|
14
|
+
def push(notification)
|
15
|
+
response = post_request payload_from(notification)
|
16
|
+
handle_error(response) unless response.code == "200"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
attr_reader :config
|
21
|
+
|
22
|
+
def payload_from(notification)
|
23
|
+
deep_compact({
|
24
|
+
message: {
|
25
|
+
token: notification.token,
|
26
|
+
data: notification.data ? stringify(notification.data) : {},
|
27
|
+
android: {
|
28
|
+
notification: {
|
29
|
+
title: notification.title,
|
30
|
+
body: notification.body,
|
31
|
+
notification_count: notification.badge,
|
32
|
+
sound: notification.sound
|
33
|
+
},
|
34
|
+
collapse_key: notification.thread_id,
|
35
|
+
priority: notification.high_priority == true ? "high" : "normal"
|
36
|
+
}
|
37
|
+
}.deep_merge(notification.google_data ? stringify_data(notification.google_data) : {})
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
def deep_compact(payload)
|
42
|
+
payload.dig(:message, :android, :notification).try(&:compact!)
|
43
|
+
payload.dig(:message, :android).try(&:compact!)
|
44
|
+
payload[:message].compact!
|
45
|
+
payload
|
46
|
+
end
|
47
|
+
|
48
|
+
# FCM requires data values to be strings.
|
49
|
+
def stringify_data(google_data)
|
50
|
+
google_data.tap do |payload|
|
51
|
+
payload[:data] = stringify(payload[:data]) if payload[:data]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def stringify(hash)
|
56
|
+
hash.compact.transform_values(&:to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
def post_request(payload)
|
60
|
+
uri = URI("https://fcm.googleapis.com/v1/projects/#{config.fetch(:project_id)}/messages:send")
|
61
|
+
request = Net::HTTP::Post.new(uri)
|
62
|
+
request["Authorization"] = "Bearer #{access_token}"
|
63
|
+
request["Content-Type"] = "application/json"
|
64
|
+
request.body = payload.to_json
|
65
|
+
|
66
|
+
rescue_and_reraise_network_errors do
|
67
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: config[:request_timeout] || DEFAULT_TIMEOUT) do |http|
|
68
|
+
http.request(request)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def rescue_and_reraise_network_errors
|
74
|
+
yield
|
75
|
+
rescue Net::ReadTimeout, Net::OpenTimeout => e
|
76
|
+
raise ActionPushNative::TimeoutError, e.message
|
77
|
+
rescue Errno::ECONNRESET, SocketError => e
|
78
|
+
raise ActionPushNative::ConnectionError, e.message
|
79
|
+
rescue OpenSSL::SSL::SSLError => e
|
80
|
+
if e.message.include?("SSL_connect")
|
81
|
+
raise ActionPushNative::ConnectionError, e.message
|
82
|
+
else
|
83
|
+
raise
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def access_token
|
88
|
+
authorizer = Google::Auth::ServiceAccountCredentials.make_creds \
|
89
|
+
json_key_io: StringIO.new(config.fetch(:encryption_key)),
|
90
|
+
scope: "https://www.googleapis.com/auth/firebase.messaging"
|
91
|
+
authorizer.fetch_access_token!["access_token"]
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_error(response)
|
95
|
+
code = response.code
|
96
|
+
reason = \
|
97
|
+
begin
|
98
|
+
JSON.parse(response.body).dig("error", "message")
|
99
|
+
rescue JSON::ParserError
|
100
|
+
response.body
|
101
|
+
end
|
102
|
+
|
103
|
+
Rails.logger.error("FCM response error #{code}: #{reason}")
|
104
|
+
|
105
|
+
case
|
106
|
+
when reason =~ /message is too big/i
|
107
|
+
raise ActionPushNative::PayloadTooLargeError, reason
|
108
|
+
when code == "400"
|
109
|
+
raise ActionPushNative::BadRequestError, reason
|
110
|
+
when code == "404"
|
111
|
+
raise ActionPushNative::TokenError, reason
|
112
|
+
when code.in?([ "401", "403" ])
|
113
|
+
raise ActionPushNative::ForbiddenError, reason
|
114
|
+
when code == "429"
|
115
|
+
raise ActionPushNative::TooManyRequestsError, reason
|
116
|
+
when code == "503"
|
117
|
+
raise ActionPushNative::ServiceUnavailableError, reason
|
118
|
+
else
|
119
|
+
raise ActionPushNative::InternalServerError, reason
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zeitwerk"
|
4
|
+
require "action_push_native/engine"
|
5
|
+
require "action_push_native/errors"
|
6
|
+
require "net/http"
|
7
|
+
require "apnotic"
|
8
|
+
require "googleauth"
|
9
|
+
|
10
|
+
loader= Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
11
|
+
loader.ignore("#{__dir__}/generators")
|
12
|
+
loader.ignore("#{__dir__}/action_push_native/errors.rb")
|
13
|
+
loader.setup
|
14
|
+
|
15
|
+
module ActionPushNative
|
16
|
+
def self.service_for(platform, notification)
|
17
|
+
platform_config = config_for(platform, notification)
|
18
|
+
|
19
|
+
case platform.to_sym
|
20
|
+
when :apple
|
21
|
+
Service::Apns.new(platform_config)
|
22
|
+
when :google
|
23
|
+
Service::Fcm.new(platform_config)
|
24
|
+
else
|
25
|
+
raise "ActionPushNative: '#{platform}' platform is unsupported"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.config_for(platform, notification)
|
30
|
+
platform_config = Rails.application.config_for(:push)[platform.to_sym]
|
31
|
+
raise "ActionPushNative: '#{platform}' platform is not configured" unless platform_config.present?
|
32
|
+
|
33
|
+
if notification.application.present?
|
34
|
+
notification_config = platform_config.fetch(notification.application.to_sym, {})
|
35
|
+
platform_config.fetch(:application, {}).merge(notification_config)
|
36
|
+
else
|
37
|
+
platform_config
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActionPushNative::InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
def copy_files
|
7
|
+
template "config/push.yml"
|
8
|
+
template "app/models/application_push_notification.rb"
|
9
|
+
template "app/models/application_push_device.rb"
|
10
|
+
template "app/jobs/application_push_notification_job.rb"
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class ApplicationPushNotification < ActionPushNative::Notification
|
2
|
+
# Set a custom job queue_name
|
3
|
+
# queue_as :realtime
|
4
|
+
|
5
|
+
# Controls whether push notifications are enabled (default: !Rails.env.test?)
|
6
|
+
# self.enabled = Rails.env.production?
|
7
|
+
|
8
|
+
# Define a custom callback to modify or abort the notification before it is sent
|
9
|
+
# before_delivery do |notification|
|
10
|
+
# throw :abort if Notification.find(notification.context[:notification_id]).expired?
|
11
|
+
# end
|
12
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
shared:
|
2
|
+
apple:
|
3
|
+
# Token auth params
|
4
|
+
# See https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns
|
5
|
+
key_id: your_key_id
|
6
|
+
encryption_key: your_apple_encryption_key
|
7
|
+
|
8
|
+
team_id: your_apple_team_id
|
9
|
+
# Your identifier found on https://developer.apple.com/account/resources/identifiers/list
|
10
|
+
topic: your.bundle.identifier
|
11
|
+
|
12
|
+
# Set this to the number of threads used to process notifications (default: 5).
|
13
|
+
# When the pool size is too small a ConnectionPool::TimeoutError error will be raised.
|
14
|
+
# connection_pool_size: 5
|
15
|
+
|
16
|
+
# Change the request timeout (default: 30).
|
17
|
+
# request_timeout: 60
|
18
|
+
|
19
|
+
# Decide when to connect to APNs development server.
|
20
|
+
# Please note that anything built directly from Xcode and loaded on your phone will have
|
21
|
+
# the app generate DEVELOPMENT tokens, while everything else (TestFlight, Apple Store, ...)
|
22
|
+
# will be considered as PRODUCTION environment.
|
23
|
+
# connect_to_development_server: <%# Rails.env.development? %>
|
24
|
+
|
25
|
+
google:
|
26
|
+
# Your Firebase project service account credentials
|
27
|
+
# See https://firebase.google.com/docs/cloud-messaging/auth-server
|
28
|
+
encryption_key: your_service_account_json_file
|
29
|
+
|
30
|
+
# Firebase project_id
|
31
|
+
project_id: your_project_id
|
32
|
+
|
33
|
+
# Change the request timeout (default: 15).
|
34
|
+
# request_timeout: 30
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: action_push_native
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jacopo Beschi
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activerecord
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '8.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '8.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activejob
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '8.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: railties
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '8.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '8.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: apnotic
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.7'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.7'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: googleauth
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.14'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.14'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: net-http
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.6'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.6'
|
96
|
+
description: Send push notifications to mobile apps
|
97
|
+
email:
|
98
|
+
- jacopo@37signals.com
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- MIT-LICENSE
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- app/jobs/action_push_native/notification_job.rb
|
107
|
+
- app/models/action_push_native/device.rb
|
108
|
+
- db/migrate/20250610075650_create_action_push_native_device.rb
|
109
|
+
- lib/action_push_native.rb
|
110
|
+
- lib/action_push_native/configured_notification.rb
|
111
|
+
- lib/action_push_native/engine.rb
|
112
|
+
- lib/action_push_native/errors.rb
|
113
|
+
- lib/action_push_native/notification.rb
|
114
|
+
- lib/action_push_native/service/apns.rb
|
115
|
+
- lib/action_push_native/service/fcm.rb
|
116
|
+
- lib/action_push_native/version.rb
|
117
|
+
- lib/generators/action_push_native/install/install_generator.rb
|
118
|
+
- lib/generators/action_push_native/install/templates/app/jobs/application_push_notification_job.rb.tt
|
119
|
+
- lib/generators/action_push_native/install/templates/app/models/application_push_device.rb.tt
|
120
|
+
- lib/generators/action_push_native/install/templates/app/models/application_push_notification.rb.tt
|
121
|
+
- lib/generators/action_push_native/install/templates/config/push.yml.tt
|
122
|
+
homepage: https://github.com/basecamp/action_push_native
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata:
|
126
|
+
homepage_uri: https://github.com/basecamp/action_push_native
|
127
|
+
source_code_uri: https://github.com/basecamp/action_push_native
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: 3.2.0
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubygems_version: 3.6.7
|
143
|
+
specification_version: 4
|
144
|
+
summary: Send push notifications to mobile apps
|
145
|
+
test_files: []
|