action_push_native 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c37771ec586061a95004904931f0ffc841177a104fa27b8f5c9404bdef33bcc8
4
- data.tar.gz: d39db792468a2516ce99d9330f0612b1ba1ef1ba194ee0cfa3b4f493164c214e
3
+ metadata.gz: bb63f1d3b677a29544c2691f401106285c40a3d2ef023f58c814d0afbb6ac500
4
+ data.tar.gz: 1a14922fd7f365aa860634cdd590da4047f03bf3ce6b200a91a103c6b17b04f0
5
5
  SHA512:
6
- metadata.gz: 761d8d0a9eeb0f4914fa2129dda438958babf02354be17ed87b8264ebd052d10ff73a98723afbeb40c6dc0253de81f26ed37fc93588b3315b08264181cee1554
7
- data.tar.gz: b0351f879b5fcf6a244c053bcd375ea4803071ec82d8d56329bd22e4eb7a346f8219f56b34497c27faf705a094490164399df7526dbf04daf89b8d73344d9330
6
+ metadata.gz: 78149ea68432d42200e2fe22cfb8d676b96acfebfcf169a17bc4b78dbd1c48eb7b082718ed1e382529e4fea0a505622319f487136e80ecef5b4ef8b169a280eb
7
+ data.tar.gz: 2f479abecd271700608b73ecd4ebba476449774dae7349951af959709886b7164f9896b88f91d04e093884ee53e953b3cc1719728d55b9591866280129c79184
data/README.md CHANGED
@@ -75,8 +75,8 @@ shared:
75
75
  apple:
76
76
  # Token auth params
77
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
78
+ key_id: <%= Rails.application.credentials.dig(:action_push_native, :apns, :key_id) %>
79
+ encryption_key: <%= Rails.application.credentials.dig(:action_push_native, :apns, :encryption_key)&.dump %>
80
80
 
81
81
  team_id: your_apple_team_id
82
82
  # Your identifier found on https://developer.apple.com/account/resources/identifiers/list
@@ -85,7 +85,7 @@ shared:
85
85
  google:
86
86
  # Your Firebase project service account credentials
87
87
  # See https://firebase.google.com/docs/cloud-messaging/auth-server
88
- encryption_key: your_service_account_json_file
88
+ encryption_key: <%= Rails.application.credentials.dig(:action_push_native, :fcm, :encryption_key)&.dump %>
89
89
 
90
90
  # Firebase project_id
91
91
  project_id: your_project_id
@@ -130,16 +130,16 @@ shared:
130
130
  calendar:
131
131
  # Token auth params
132
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
133
+ key_id: <%= Rails.application.credentials.dig(:action_push_native, :apns, :calendar, :key_id) %>
134
+ encryption_key: <%= Rails.application.credentials.dig(:action_push_native, :apns, :calendar, :encryption_key)&.dump %>
135
135
  # Your identifier found on https://developer.apple.com/account/resources/identifiers/list
136
136
  topic: calendar.bundle.identifier
137
137
 
138
138
  email:
139
139
  # Token auth params
140
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
141
+ key_id: <%= Rails.application.credentials.dig(:action_push_native, :apns, :email, :key_id) %>
142
+ encryption_key: <%= Rails.application.credentials.dig(:action_push_native, :apns, :email, :encryption_key)&.dump %>
143
143
  # Your identifier found on https://developer.apple.com/account/resources/identifiers/list
144
144
  topic: email.bundle.identifier
145
145
 
@@ -147,7 +147,7 @@ shared:
147
147
  calendar:
148
148
  # Your Firebase project service account credentials
149
149
  # See https://firebase.google.com/docs/cloud-messaging/auth-server
150
- encryption_key: calendar_service_account_json_file
150
+ encryption_key: <%= Rails.application.credentials.dig(:action_push_native, :fcm, :calendar, :encryption_key)&.dump %>
151
151
 
152
152
  # Firebase project_id
153
153
  project_id: calendar_project_id
@@ -155,7 +155,7 @@ shared:
155
155
  email:
156
156
  # Your Firebase project service account credentials
157
157
  # See https://firebase.google.com/docs/cloud-messaging/auth-server
158
- encryption_key: email_service_account_json_file
158
+ encryption_key: <%= Rails.application.credentials.dig(:action_push_native, :fcm, :email, :encryption_key)&.dump %>
159
159
 
160
160
  # Firebase project_id
161
161
  project_id: email_project_id
@@ -212,7 +212,7 @@ You can use `with_apple` for Apple and `with_google` for Google:
212
212
 
213
213
  ```ruby
214
214
  notification = ApplicationPushNotification
215
- .with_apple(category: "observable")
215
+ .with_apple(aps: { category: "observable", "thread-id": "greeting"}, "apns-priority": "1")
216
216
  .with_google(data: { badge: 1 })
217
217
  .new(title: "Hello world!")
218
218
  ```
@@ -234,7 +234,7 @@ and `body`.
234
234
  You can create a silent notification via the `silent` method:
235
235
 
236
236
  ```ruby
237
- notification = ApplicationPushNotification.silent.with_data(id: 1)
237
+ notification = ApplicationPushNotification.silent.with_data(id: 1).new
238
238
  ```
239
239
 
240
240
  This will create a silent notification for both Apple and Google platforms and sets an application
@@ -271,7 +271,7 @@ by adding extra arguments to the notification constructor:
271
271
  data = { calendar_id: @calendar.id, identity_id: @identity.id }
272
272
 
273
273
  notification = CalendarPushNotification
274
- .with_apple(custom_payload: data)
274
+ .with_apple(data)
275
275
  .with_google(data: data)
276
276
  .new(calendar_id: 123)
277
277
 
@@ -311,7 +311,7 @@ end
311
311
  | :sound | The sound to play when the notification is received.
312
312
  | :high_priority | Whether the notification should be sent with high priority (default: true).
313
313
  | :google_data | The Google-specific payload for the notification.
314
- | :apple_data | The Apple-specific payload for the notification.
314
+ | :apple_data | The Apple-specific payload for the notification. It can also be used to override APNs request headers, such as `apns-push-type`, `apns-priority`, etc.
315
315
  | :data | The data payload for the notification, sent to all platforms.
316
316
  | ** | Any additional attributes passed to the constructor will be merged in the `context` hash.
317
317
 
@@ -320,7 +320,7 @@ end
320
320
  | Name | Description
321
321
  |------------------|------------
322
322
  | :with_apple | Set the Apple-specific payload for the notification.
323
- | :with_google | Set the Google-specific payload for the notification.
323
+ | :with_google | Set the Google-specific payload for the notification. It can also be used to override APNs request headers, such as `apns-push-type`, `apns-priority`, etc.
324
324
  | :with_data | Set the data payload for the notification, sent to all platforms.
325
325
  | :silent | Create a silent notification that does not trigger a visual alert on the device.
326
326
 
@@ -40,7 +40,7 @@ module ActionPushNative
40
40
 
41
41
  with_options retry_options do
42
42
  retry_on TimeoutError, wait: 1.minute
43
- retry_on ConnectionError, ConnectionPool::TimeoutError, attempts: 20
43
+ retry_on ConnectionError, HTTPX::PoolTimeoutError, attempts: 20
44
44
 
45
45
  # Altough unexpected, these are short-lived errors that can be retried most of the times.
46
46
  retry_on ForbiddenError, BadRequestError
@@ -16,7 +16,7 @@ module ActionPushNative
16
16
 
17
17
  def silent
18
18
  @options = options.merge(high_priority: false)
19
- with_apple(content_available: 1)
19
+ with_apple(aps: { "content-available": 1 })
20
20
  end
21
21
 
22
22
  def with_apple(apple_data)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Converts the legacy `apple_data` format from the Apnotic gem
4
+ # to the new format expected by the APNs API.
5
+ #
6
+ # Temporary compatibility layer: It will be removed in the next release.
7
+ class ActionPushNative::Service::Apns::ApnoticLegacyConverter
8
+ APS_FIELDS = %i[
9
+ alert badge sound content_available category url_args mutable_content thread_id
10
+ target_content_id interruption_level relevance_score
11
+ stale_date content_state timestamp event dismissal_date
12
+ ].freeze
13
+ APNS_HEADERS = %i[ expiration priority topic push_type ]
14
+
15
+ def self.convert(apple_data)
16
+ apple_data.each_with_object({}) do |(key, value), converted|
17
+ if key.in?(APS_FIELDS)
18
+ converted[:aps] ||= {}
19
+ converted_key = key.to_s.dasherize.to_sym
20
+ converted[:aps][converted_key] = value
21
+ ActionPushNative.deprecator.warn("Passing the `#{key}` field directly is deprecated. Please use `.with_apple(aps: { \"#{converted_key}\": ... })` instead.")
22
+ elsif key.in?(APNS_HEADERS)
23
+ converted_key = "apns-#{key.to_s.dasherize}".to_sym
24
+ converted[converted_key] = value
25
+ ActionPushNative.deprecator.warn("Passing the `#{key}` header directly is deprecated. Please use `.with_apple(\"#{converted_key}\": ...)` instead.")
26
+ elsif key == :apns_collapse_id
27
+ converted_key = key.to_s.dasherize.to_sym
28
+ converted[converted_key] = value
29
+ ActionPushNative.deprecator.warn("Passing the `#{key}` header directly is deprecated. Please use `.with_apple(\"#{converted_key}\": ...)` instead.")
30
+ elsif key == :custom_payload
31
+ converted.merge!(value)
32
+ ActionPushNative.deprecator.warn("Passing `custom_payload` is deprecated. Please use `.with_apple(#{value})` instead.")
33
+ else
34
+ converted[key] = value
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActionPushNative::Service::Apns::HttpxSession
4
+ DEFAULT_POOL_SIZE = 5
5
+ DEFAULT_REQUEST_TIMEOUT = 30.seconds
6
+ DEVELOPMENT_SERVER_URL = "https://api.sandbox.push.apple.com:443"
7
+ PRODUCTION_SERVER_URL = "https://api.push.apple.com:443"
8
+
9
+ def initialize(config)
10
+ @session = \
11
+ HTTPX.
12
+ plugin(:persistent, close_on_fork: true).
13
+ with(pool_options: { max_connections: config[:connection_pool_size] || DEFAULT_POOL_SIZE }).
14
+ with(timeout: { request_timeout: config[:request_timeout] || DEFAULT_REQUEST_TIMEOUT }).
15
+ with(origin: config[:connect_to_development_server] ? DEVELOPMENT_SERVER_URL : PRODUCTION_SERVER_URL)
16
+ @token_provider = ActionPushNative::Service::Apns::TokenProvider.new(config)
17
+ end
18
+
19
+ def post(*uri, **options)
20
+ options[:headers][:authorization] = "Bearer #{token_provider.fresh_access_token}"
21
+ session.post(*uri, **options)
22
+ end
23
+
24
+ private
25
+ attr_reader :token_provider, :session
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActionPushNative::Service::Apns::TokenProvider
4
+ EXPIRED = -1
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ @expires_at = EXPIRED
9
+ end
10
+
11
+ def fresh_access_token
12
+ regenerate_if_expired
13
+ token
14
+ end
15
+
16
+ private
17
+ attr_reader :config, :token, :expires_at
18
+
19
+ # See https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns#Refresh-your-token-regularly
20
+ def regenerate_if_expired
21
+ if Time.now.utc >= expires_at
22
+ @expires_at = 30.minutes.from_now.utc
23
+ @token = generate
24
+ end
25
+ end
26
+
27
+ def generate
28
+ payload = { iss: config.fetch(:team_id), iat: Time.now.utc.to_i }
29
+ header = { kid: config.fetch(:key_id) }
30
+ private_key = OpenSSL::PKey::EC.new(config.fetch(:encryption_key))
31
+ JWT.encode(payload, private_key, "ES256", header)
32
+ end
33
+ end
@@ -3,118 +3,99 @@
3
3
  module ActionPushNative
4
4
  module Service
5
5
  class Apns
6
- DEFAULT_TIMEOUT = 30.seconds
7
- DEFAULT_POOL_SIZE = 5
6
+ include NetworkErrorHandling
7
+
8
+ # Per-application HTTPX session
9
+ cattr_accessor :httpx_sessions
8
10
 
9
11
  def initialize(config)
10
12
  @config = config
11
13
  end
12
14
 
13
- # Per-application connection pools
14
- cattr_accessor :connection_pools
15
-
16
15
  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
16
+ notification.apple_data = ApnoticLegacyConverter.convert(notification.apple_data) if notification.apple_data.present?
17
+
18
+ headers, payload = headers_from(notification), payload_from(notification)
19
+ Rails.logger.info("Pushing APNs notification: #{headers[:"apns-id"]}")
20
+ response = httpx_session.post("3/device/#{notification.token}", json: payload, headers: headers)
21
+ handle_error(response) if response.error
31
22
  end
32
23
 
33
24
  private
34
- attr_reader :config, :connection_error
35
-
36
- def reset_connection_error
37
- @connection_error = nil
38
- end
25
+ attr_reader :config
39
26
 
40
- def connection_pool
41
- self.class.connection_pools ||= {}
42
- self.class.connection_pools[config] ||= build_connection_pool
27
+ PRIORITIES = { high: 10, normal: 5 }.freeze
28
+ HEADERS = %i[ apns-id apns-push-type apns-priority apns-topic apns-expiration apns-collapse-id ].freeze
29
+
30
+ def headers_from(notification)
31
+ push_type = notification.apple_data&.dig(:aps, :"content-available") == 1 ? "background" : "alert"
32
+ custom_apple_headers = notification.apple_data&.slice(*HEADERS) || {}
33
+
34
+ {
35
+ "apns-push-type": push_type,
36
+ "apns-id": SecureRandom.uuid,
37
+ "apns-priority": notification.high_priority ? PRIORITIES[:high] : PRIORITIES[:normal],
38
+ "apns-topic": config.fetch(:topic)
39
+ }.merge(custom_apple_headers).compact
43
40
  end
44
41
 
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
42
+ def payload_from(notification)
43
+ payload = \
44
+ {
45
+ aps: {
46
+ alert: { title: notification.title, body: notification.body },
47
+ badge: notification.badge,
48
+ "thread-id": notification.thread_id,
49
+ sound: notification.sound
50
+ }
51
+ }
52
+
53
+ payload = payload.merge notification.data if notification.data.present?
54
+ custom_apple_payload = notification.apple_data&.except(*HEADERS) || {}
55
+ payload = payload.deep_merge custom_apple_payload
56
+
57
+ payload.dig(:aps, :alert)&.compact!
58
+ payload[:aps]&.compact_blank!
59
+ payload.compact
57
60
  end
58
61
 
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
62
+ def httpx_session
63
+ self.class.httpx_sessions ||= {}
64
+ self.class.httpx_sessions[config] ||= HttpxSession.new(config)
73
65
  end
74
66
 
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
67
+ def handle_error(response)
68
+ if response.is_a?(HTTPX::ErrorResponse)
69
+ handle_network_error(response.error)
70
+ else
71
+ handle_apns_error(response)
89
72
  end
90
73
  end
91
74
 
92
- def handle_response_error(response)
93
- code = response&.status
94
- reason = response.body["reason"] if response
75
+ def handle_apns_error(response)
76
+ status = response.status
77
+ reason = JSON.parse(response.body.to_s)["reason"] unless response.body.empty?
95
78
 
96
- Rails.logger.error("APNs response error #{code}: #{reason}") if reason
79
+ Rails.logger.error("APNs response error #{status}: #{reason}") if reason
97
80
 
98
- case [ code, reason ]
99
- in [ nil, _ ]
100
- raise ActionPushNative::TimeoutError
101
- in [ "400", "BadDeviceToken" ]
81
+ case [ status, reason ]
82
+ in [ 400, "BadDeviceToken" ]
102
83
  raise ActionPushNative::TokenError, reason
103
- in [ "400", "DeviceTokenNotForTopic" ]
84
+ in [ 400, "DeviceTokenNotForTopic" ]
104
85
  raise ActionPushNative::BadDeviceTopicError, reason
105
- in [ "400", _ ]
86
+ in [ 400, _ ]
106
87
  raise ActionPushNative::BadRequestError, reason
107
- in [ "403", _ ]
88
+ in [ 403, _ ]
108
89
  raise ActionPushNative::ForbiddenError, reason
109
- in [ "404", _ ]
90
+ in [ 404, _ ]
110
91
  raise ActionPushNative::NotFoundError, reason
111
- in [ "410", _ ]
92
+ in [ 410, _ ]
112
93
  raise ActionPushNative::TokenError, reason
113
- in [ "413", _ ]
94
+ in [ 413, _ ]
114
95
  raise ActionPushNative::PayloadTooLargeError, reason
115
- in [ "429", _ ]
96
+ in [ 429, _ ]
116
97
  raise ActionPushNative::TooManyRequestsError, reason
117
- in [ "503", _ ]
98
+ in [ 503, _ ]
118
99
  raise ActionPushNative::ServiceUnavailableError, reason
119
100
  else
120
101
  raise ActionPushNative::InternalServerError, reason
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActionPushNative::Service::Fcm::HttpxSession
4
+ # FCM suggests at least a 10s timeout for requests, we set 15 to add some buffer.
5
+ # https://firebase.google.com/docs/cloud-messaging/scale-fcm#timeouts
6
+ DEFAULT_REQUEST_TIMEOUT = 15.seconds
7
+ DEFAULT_POOL_SIZE = 5
8
+
9
+ def initialize(config)
10
+ @session = \
11
+ HTTPX.
12
+ plugin(:persistent, close_on_fork: true).
13
+ with(timeout: { request_timeout: config[:request_timeout] || DEFAULT_REQUEST_TIMEOUT }).
14
+ with(pool_options: { max_connections: config[:connection_pool_size] || DEFAULT_POOL_SIZE }).
15
+ with(origin: "https://fcm.googleapis.com")
16
+ @token_provider = ActionPushNative::Service::Fcm::TokenProvider.new(config)
17
+ end
18
+
19
+ def post(*uri, **options)
20
+ options[:headers] ||= {}
21
+ options[:headers][:authorization] = "Bearer #{token_provider.fresh_access_token}"
22
+ session.post(*uri, **options)
23
+ end
24
+
25
+ private
26
+ attr_reader :token_provider, :session
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActionPushNative::Service::Fcm::TokenProvider
4
+ EXPIRED = -1
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ @expires_at = EXPIRED
9
+ end
10
+
11
+ def fresh_access_token
12
+ regenerate_if_expired
13
+ token
14
+ end
15
+
16
+ private
17
+ attr_reader :config, :token, :expires_at
18
+
19
+ def regenerate_if_expired
20
+ regenerate if Time.now.utc >= expires_at
21
+ end
22
+
23
+ REFRESH_BUFFER = 1.minutes
24
+
25
+ def regenerate
26
+ authorizer = Google::Auth::ServiceAccountCredentials.make_creds \
27
+ json_key_io: StringIO.new(config.fetch(:encryption_key)),
28
+ scope: "https://www.googleapis.com/auth/firebase.messaging"
29
+ oauth2 = authorizer.fetch_access_token!
30
+ @token = oauth2["access_token"]
31
+ @expires_at = oauth2["expires_in"].seconds.from_now.utc - REFRESH_BUFFER
32
+ end
33
+ end
@@ -3,22 +3,28 @@
3
3
  module ActionPushNative
4
4
  module Service
5
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
6
+ include NetworkErrorHandling
7
+
8
+ # Per-application HTTPX session
9
+ cattr_accessor :httpx_sessions
9
10
 
10
11
  def initialize(config)
11
12
  @config = config
12
13
  end
13
14
 
14
15
  def push(notification)
15
- response = post_request payload_from(notification)
16
- handle_error(response) unless response.code == "200"
16
+ response = httpx_session.post("v1/projects/#{config.fetch(:project_id)}/messages:send", json: payload_from(notification))
17
+ handle_error(response) if response.error
17
18
  end
18
19
 
19
20
  private
20
21
  attr_reader :config
21
22
 
23
+ def httpx_session
24
+ self.class.httpx_sessions ||= {}
25
+ self.class.httpx_sessions[config] ||= HttpxSession.new(config)
26
+ end
27
+
22
28
  def payload_from(notification)
23
29
  deep_compact({
24
30
  message: {
@@ -56,64 +62,37 @@ module ActionPushNative
56
62
  hash.compact.transform_values(&:to_s)
57
63
  end
58
64
 
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
65
+ def handle_error(response)
66
+ if response.is_a?(HTTPX::ErrorResponse)
67
+ handle_network_error(response.error)
82
68
  else
83
- raise
69
+ handle_fcm_error(response)
84
70
  end
85
71
  end
86
72
 
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
73
+ def handle_fcm_error(response)
74
+ status = response.status
96
75
  reason = \
97
76
  begin
98
- JSON.parse(response.body).dig("error", "message")
77
+ JSON.parse(response.body.to_s).dig("error", "message")
99
78
  rescue JSON::ParserError
100
- response.body
79
+ response.body.to_s
101
80
  end
102
81
 
103
- Rails.logger.error("FCM response error #{code}: #{reason}")
82
+ Rails.logger.error("FCM response error #{status}: #{reason}")
104
83
 
105
84
  case
106
85
  when reason =~ /message is too big/i
107
86
  raise ActionPushNative::PayloadTooLargeError, reason
108
- when code == "400"
87
+ when status == 400
109
88
  raise ActionPushNative::BadRequestError, reason
110
- when code == "404"
89
+ when status == 404
111
90
  raise ActionPushNative::TokenError, reason
112
- when code.in?([ "401", "403" ])
91
+ when status.in?([ 401, 403 ])
113
92
  raise ActionPushNative::ForbiddenError, reason
114
- when code == "429"
93
+ when status == 429
115
94
  raise ActionPushNative::TooManyRequestsError, reason
116
- when code == "503"
95
+ when status == 503
117
96
  raise ActionPushNative::ServiceUnavailableError, reason
118
97
  else
119
98
  raise ActionPushNative::InternalServerError, reason
@@ -0,0 +1,22 @@
1
+ module ActionPushNative::Service::NetworkErrorHandling
2
+ private
3
+
4
+ def handle_network_error(error)
5
+ case error
6
+ when HTTPX::PoolTimeoutError
7
+ raise error
8
+ when Errno::ETIMEDOUT, HTTPX::TimeoutError
9
+ raise ActionPushNative::TimeoutError, error.message
10
+ when Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
11
+ SocketError, IOError, EOFError, Errno::EPIPE, Errno::EINVAL, HTTPX::ConnectionError,
12
+ HTTPX::TLSError, HTTPX::Connection::HTTP2::Error
13
+ raise ActionPushNative::ConnectionError, error.message
14
+ when OpenSSL::SSL::SSLError
15
+ if error.message.include?("SSL_connect")
16
+ raise ActionPushNative::ConnectionError, error.message
17
+ else
18
+ raise
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module ActionPushNative
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -3,9 +3,9 @@
3
3
  require "zeitwerk"
4
4
  require "action_push_native/engine"
5
5
  require "action_push_native/errors"
6
- require "net/http"
7
- require "apnotic"
6
+ require "httpx"
8
7
  require "googleauth"
8
+ require "jwt"
9
9
 
10
10
  loader= Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
11
11
  loader.ignore("#{__dir__}/generators")
@@ -37,4 +37,8 @@ module ActionPushNative
37
37
  platform_config
38
38
  end
39
39
  end
40
+
41
+ def self.deprecator
42
+ @deprecator ||= ActiveSupport::Deprecation.new
43
+ end
40
44
  end
@@ -1,16 +1,17 @@
1
1
  shared:
2
+ # Use bin/rails credentials:edit to set the apns secrets (as action_push_native:apns:key_id|encryption_key)
2
3
  apple:
3
4
  # Token auth params
4
5
  # 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
6
+ key_id: <%%= Rails.application.credentials.dig(:action_push_native, :apns, :key_id) %>
7
+ encryption_key: <%%= Rails.application.credentials.dig(:action_push_native, :apns, :encryption_key)&.dump %>
7
8
 
8
9
  team_id: your_apple_team_id
9
10
  # Your identifier found on https://developer.apple.com/account/resources/identifiers/list
10
11
  topic: your.bundle.identifier
11
12
 
12
13
  # 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
+ # When the pool size is too small a HTTPX::PoolTimeoutError error will be raised.
14
15
  # connection_pool_size: 5
15
16
 
16
17
  # Change the request timeout (default: 30).
@@ -20,15 +21,20 @@ shared:
20
21
  # Please note that anything built directly from Xcode and loaded on your phone will have
21
22
  # the app generate DEVELOPMENT tokens, while everything else (TestFlight, Apple Store, ...)
22
23
  # will be considered as PRODUCTION environment.
23
- # connect_to_development_server: <%# Rails.env.development? %>
24
+ # connect_to_development_server: <%%# Rails.env.development? %>
24
25
 
26
+ # Use bin/rails credentials:edit to set the fcm secrets (as action_push_native:fcm:encryption_key)
25
27
  google:
26
28
  # Your Firebase project service account credentials
27
29
  # See https://firebase.google.com/docs/cloud-messaging/auth-server
28
- encryption_key: your_service_account_json_file
30
+ encryption_key: <%%= Rails.application.credentials.dig(:action_push_native, :fcm, :encryption_key)&.dump %>
29
31
 
30
32
  # Firebase project_id
31
33
  project_id: your_project_id
32
34
 
35
+ # Set this to the number of threads used to process notifications (default: 5).
36
+ # When the pool size is too small a HTTPX::PoolTimeoutError error will be raised.
37
+ # connection_pool_size: 5
38
+
33
39
  # Change the request timeout (default: 15).
34
40
  # request_timeout: 30
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_push_native
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacopo Beschi
@@ -52,47 +52,47 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '8.0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: apnotic
55
+ name: httpx
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1.7'
60
+ version: '1.6'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '1.7'
67
+ version: '1.6'
68
68
  - !ruby/object:Gem::Dependency
69
- name: googleauth
69
+ name: jwt
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
- - - "~>"
72
+ - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: '1.14'
74
+ version: '2'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
- - - "~>"
79
+ - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: '1.14'
81
+ version: '2'
82
82
  - !ruby/object:Gem::Dependency
83
- name: net-http
83
+ name: googleauth
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: '0.6'
88
+ version: '1.14'
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: '0.6'
95
+ version: '1.14'
96
96
  description: Send push notifications to mobile apps
97
97
  email:
98
98
  - jacopo@37signals.com
@@ -112,7 +112,13 @@ files:
112
112
  - lib/action_push_native/errors.rb
113
113
  - lib/action_push_native/notification.rb
114
114
  - lib/action_push_native/service/apns.rb
115
+ - lib/action_push_native/service/apns/apnotic_legacy_converter.rb
116
+ - lib/action_push_native/service/apns/httpx_session.rb
117
+ - lib/action_push_native/service/apns/token_provider.rb
115
118
  - lib/action_push_native/service/fcm.rb
119
+ - lib/action_push_native/service/fcm/httpx_session.rb
120
+ - lib/action_push_native/service/fcm/token_provider.rb
121
+ - lib/action_push_native/service/network_error_handling.rb
116
122
  - lib/action_push_native/version.rb
117
123
  - lib/generators/action_push_native/install/install_generator.rb
118
124
  - lib/generators/action_push_native/install/templates/app/jobs/application_push_notification_job.rb.tt
@@ -139,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
145
  - !ruby/object:Gem::Version
140
146
  version: '0'
141
147
  requirements: []
142
- rubygems_version: 3.6.7
148
+ rubygems_version: 3.6.9
143
149
  specification_version: 4
144
150
  summary: Send push notifications to mobile apps
145
151
  test_files: []