From e54236cf68f4c24fc2cb597d1e4373259a42b7c6 Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 08:20:41 +0000 Subject: [PATCH 1/8] Change :format to :json, to match v2.0 standard --- lib/noticed/delivery_methods/ios.rb | 2 +- test/delivery_methods/ios_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/noticed/delivery_methods/ios.rb b/lib/noticed/delivery_methods/ios.rb index ccf79dfd..32e17fde 100644 --- a/lib/noticed/delivery_methods/ios.rb +++ b/lib/noticed/delivery_methods/ios.rb @@ -32,7 +32,7 @@ def deliver def format_notification(apn) apn.topic = evaluate_option(:bundle_identifier) - if (method = config[:format]) + if (method = config[:json]) notification.instance_exec(apn, &method) elsif notification.params.try(:has_key?, :message) apn.alert = notification.params[:message] diff --git a/test/delivery_methods/ios_test.rb b/test/delivery_methods/ios_test.rb index 34927364..7c19cb1d 100644 --- a/test/delivery_methods/ios_test.rb +++ b/test/delivery_methods/ios_test.rb @@ -43,7 +43,7 @@ def ok? team_id: "team_id", apns_key: "apns_key", device_tokens: [:a, :b], - format: ->(apn) { + json: ->(apn) { apn.alert = "Hello world" apn.custom_payload = {url: root_url(host: "example.org")} }, From ce0d217450f963c968b6b370bf03573431673ba1 Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 08:23:20 +0000 Subject: [PATCH 2/8] Allow iOS JSON to be a method --- lib/noticed/delivery_methods/ios.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/noticed/delivery_methods/ios.rb b/lib/noticed/delivery_methods/ios.rb index 32e17fde..2b28e7a4 100644 --- a/lib/noticed/delivery_methods/ios.rb +++ b/lib/noticed/delivery_methods/ios.rb @@ -33,6 +33,7 @@ def format_notification(apn) apn.topic = evaluate_option(:bundle_identifier) if (method = config[:json]) + method = event.send(method, apn) if method.is_a?(Symbol) && event.respond_to?(method) notification.instance_exec(apn, &method) elsif notification.params.try(:has_key?, :message) apn.alert = notification.params[:message] From 2cc7f86902a59eb40fdcbcb9e43396ac586e8571 Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 08:29:33 +0000 Subject: [PATCH 3/8] Allow other iOS options to be methods --- lib/noticed/delivery_methods/ios.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/noticed/delivery_methods/ios.rb b/lib/noticed/delivery_methods/ios.rb index 2b28e7a4..8d442898 100644 --- a/lib/noticed/delivery_methods/ios.rb +++ b/lib/noticed/delivery_methods/ios.rb @@ -71,9 +71,9 @@ def new_connection_pool(development:) def connection_pool_options { auth_method: :token, - cert_path: StringIO.new(config.fetch(:apns_key)), - key_id: config.fetch(:key_id), - team_id: config.fetch(:team_id) + cert_path: StringIO.new(evaluate_option(:apns_key)), + key_id: evaluate_option(:key_id), + team_id: evaluate_option(:team_id) } end From be34545e304ab1ed450cf0eaab82aec5db434ba3 Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 08:53:42 +0000 Subject: [PATCH 4/8] Update Delivery Method Configuration section in Upgrade.md to add information and example about using a symbol to call a method --- UPGRADE.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index cec37485..12e8d098 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -80,6 +80,9 @@ end Configuration for each delivery method can be contained within a block now. This improves organization for delivery method options by defining them in the block. Procs/Lambdas will be evaluated when needed and symbols can be used to call a method. +If you are using a symbol to call a method, we pass the notification object as an argument to the method. This allows you to access the notification object within the method. +Your method must accept a single argument. If you don't need to use the object you can just use `(*)`. + ```ruby class CommentNotifier < Noticed::Event deliver_by :action_cable do |config| @@ -90,7 +93,79 @@ class CommentNotifier < Noticed::Event def to_websocket { foo: :bar } + end +end +``` + +```ruby +class CommentNotifier < Noticed::Event + include IosNotifier + + def data_only? + false + end + + def url + comment_thread_path(record.thread) + end +end + +module IosNotifier + extend ActiveSupport::Concern + + included do + deliver_by :ios do |config| + config.format = :ios_format + config.apns_key = :ios_cert + config.key_id = :ios_key_id + config.team_id = :ios_team_id + config.bundle_identifier = :ios_bundle_id + config.device_tokens = :ios_device_tokens + config.if = :send_ios_notification? + end end + + def ios_format(apn) + apn.alert = { title:, body: } unless data_only? + apn.mutable_content = true + apn.content_available = true + apn.sound = "notification.m4r" + apn.custom_payload = { + url:, + type: self.class.name, + id: record.id, + image_url: "" || image_url, + params: params.to_json + } + end + + def ios_cert(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :apns_token_cert) + end + + def ios_key_id(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :key_id) + end + + def ios_team_id(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :team_id) + end + + def ios_bundle_id(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :bundle_identifier) + end + + def ios_device_tokens(notification) + notification.recipient.ios_device_tokens + end + + def send_ios_notification?(notification) + recipient = notification.recipient + return false unless recipient.is_a?(User) + + recipient.send_notifications? + end +end ``` ### Deliver Later From 31552397b39d8688d4fba251950333af9d947c77 Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 09:15:11 +0000 Subject: [PATCH 5/8] Update Readme with information about extracting delivery methods for reusablity and configuration of delivery methods --- README.md | 120 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 110 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 676b84a5..9651b137 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ To start, generate a Notifier: rails generate noticed:notifier NewCommentNotifier ``` -#### Notifier Objects +### Notifier Objects Notifiers are essentially the controllers of the Noticed ecosystem and represent an Event. As such, we recommend naming them with the event they model in mind — be it a `NewSaleNotifier,` `ChargeFailureNotifier`, etc. @@ -117,7 +117,75 @@ end For deeper specifics on setting up the `:action_cable`, `:email`, and `:discord` (bulk) delivery methods, refer to their docs: [`action_cable`](docs/delivery_methods/action_cable.md), [`email`](docs/delivery_methods/email.md), and [`discord` (bulk)](docs/bulk_delivery_methods/discord.md). -##### Required Params +#### Delivery Method Configuration + +Each delivery method can be configured with a block that yields a `config` object. + +Procs/Lambdas will be evaluated when needed and symbols can be used to call a method. + +If you are using a symbol to call a method, we pass the notification object as an argument to the method. This allows you to access the notification object within the method. +Your method must accept a single argument. If you don't need to use the object you can just use `(*)`. + +```ruby +class CommentNotifier < Noticed::Event + deliver_by :ios do |config| + config.format = :ios_format + config.apns_key = :ios_cert + config.key_id = :ios_key_id + config.team_id = :ios_team_id + config.bundle_identifier = :ios_bundle_id + config.device_tokens = :ios_device_tokens + config.if = :send_ios_notification? + end + + def ios_format(apn) + apn.alert = { title:, body: } + apn.mutable_content = true + apn.content_available = true + apn.sound = "notification.m4r" + apn.custom_payload = { + url:, + type: self.class.name, + id: record.id, + image_url: "" || image_url, + params: params.to_json + } + end + + def ios_cert(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :apns_token_cert) + end + + def ios_key_id(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :key_id) + end + + def ios_team_id(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :team_id) + end + + def ios_bundle_id(*) + Rails.application.credentials.dig(:ios, Rails.env.to_sym, :bundle_identifier) + end + + def ios_device_tokens(notification) + notification.recipient.ios_device_tokens + end + + def send_ios_notification?(notification) + recipient = notification.recipient + return false unless recipient.is_a?(User) + + recipient.send_notifications? + end + + def url + comment_thread_path(record.thread) + end +end +``` + +#### Required Params While explicit / required parameters are completely optional, Notifiers are able to opt in to required parameters via the `required_params` method: @@ -143,9 +211,7 @@ CarSaleNotifier.with(record: Car.last, branch: Branch.last).deliver(Branch.hq) #=> OK ``` - - -##### Helper Methods +#### Helper Methods Notifiers can implement various helper methods, within a `notification_methods` block, that make it easier to render the resulting notification directly. These helpers can be helpful depending on where and how you choose to render notifications. A common use is rendering a user’s notifications in your web UI as standard ERB. These notification helper methods make that rendering much simpler: @@ -159,7 +225,7 @@ Notifiers can implement various helper methods, within a `notification_methods` On the other hand, if you’re using email delivery, ActionMailer has its own full stack for setting up objects and rendering. Your notification helper methods will always be available from the notification object, but using ActionMailer’s own paradigms may fit better for that particular delivery method. YMMV. -###### URL Helpers +##### URL Helpers Rails url helpers are included in Notifiers by default so you have full access to them in your notification helper methods, just like you would in your controllers and views. @@ -169,7 +235,7 @@ _But don't forget_, you'll need to configure `default_url_options` in order for Rails.application.routes.default_url_options[:host] = 'localhost:3000' ``` -###### Translations +##### Translations We've also included Rails’ `translate` and `t` helpers for you to use in your notification helper methods. This also provides an easy way of scoping translations. If the key starts with a period, it will automatically scope the key under `notifiers`, the underscored name of the notifier class, and `notification`. For example: @@ -209,7 +275,7 @@ en: Or, if you have your Notifier within another module, such as `Admin::NewCommentNotifier`, the resulting lookup path will be `en.notifiers.admin.new_comment_notifier.notification.message` (modules become nesting steps). -##### Tip: Capture User Preferences +#### Tip: Capture User Preferences You can use the `if:` and `unless: ` options on your delivery methods to check the user's preferences and skip processing if they have disabled that type of notification. @@ -224,8 +290,42 @@ class CommentNotifier < Noticed::Event end end ``` +#### Tip: Extracting Delivery Method Configurations -**Shared Delivery Method Options** +If you want to reuse delivery method configurations across multiple Notifiers, you can extract them into a module and include them in your Notifiers. + +```ruby +# /app/notifiers/notifiers/comment_notifier.rb +class CommentNotifier < Noticed::Event + include IosNotifier + include AndriodNotifer + include EmailNotifier + + validates :record, presence: true +end + +# /app/notifiers/concerns/ios_notifier.rb +module IosNotifier + extend ActiveSupport::Concern + + included do + deliver_by :ios do |config| + config.device_tokens = ->(recipient) { recipient.notification_tokens.where(platform: :iOS).pluck(:token) } + config.format = ->(apn) { + apn.alert = "Hello world" + apn.custom_payload = {url: root_url(host: "example.org")} + } + config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_id) + config.key_id = Rails.application.credentials.dig(:ios, :key_id) + config.team_id = Rails.application.credentials.dig(:ios, :team_id) + config.apns_key = Rails.application.credentials.dig(:ios, :apns_key) + config.if = ->(recipient) { recipient.ios_notifications? } + end + end +end +``` + +#### Shared Delivery Method Options Each of these options are available for every delivery method (individual or bulk). The value passed may be a lambda, a symbol that represents a callable method, a symbol value, or a string value. @@ -235,7 +335,7 @@ Each of these options are available for every delivery method (individual or bul * `config.wait_until` — (Should yield a specific time object) Delays the job that runs this delivery method until the specific time specified * `config.queue` — Sets the ActiveJob queue name to be used for the job that runs this delivery method -#### Sending Notifications +### Sending Notifications Following the `NewCommentNotifier` example above, here’s how we might invoke the Notifier to send notifications to every author in the thread about a new comment being added: From 590d052d2521d7368f1bd7df10e13563438cc942 Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 09:29:32 +0000 Subject: [PATCH 6/8] Correct .format to .json in ios.md --- docs/delivery_methods/ios.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/delivery_methods/ios.md b/docs/delivery_methods/ios.md index de0681da..050fbc77 100644 --- a/docs/delivery_methods/ios.md +++ b/docs/delivery_methods/ios.md @@ -22,7 +22,7 @@ https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_ class CommentNotification deliver_by :ios do |config| config.device_tokens = ->(recipient) { recipient.notification_tokens.where(platform: :iOS).pluck(:token) } - config.format = ->(apn) { + config.json = ->(apn) { apn.alert = "Hello world" apn.custom_payload = {url: root_url(host: "example.org")} } From 5f7eca8ea77ab9abf04bd34513c5546e13c66f6d Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 20:46:35 +0000 Subject: [PATCH 7/8] revert 'json' to 'format' for ios deliveries --- UPGRADE.md | 1 - docs/delivery_methods/ios.md | 4 ++-- lib/noticed/delivery_methods/ios.rb | 2 +- test/delivery_methods/ios_test.rb | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 12e8d098..7684369d 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -306,7 +306,6 @@ The `invalid_token` option replaces the `cleanup_device_tokens` method for handl #### iOS The `cert_path` option has been renamed to `apns_key` and should be given the key and not a path. -The `format` option has been renamed to `json`. The `device_tokens` option is now required and should return an Array of device tokens. The `invalid_token` option replaces the `cleanup_device_tokens` method for handling invalid/expired tokens. diff --git a/docs/delivery_methods/ios.md b/docs/delivery_methods/ios.md index 050fbc77..81c86118 100644 --- a/docs/delivery_methods/ios.md +++ b/docs/delivery_methods/ios.md @@ -22,7 +22,7 @@ https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_ class CommentNotification deliver_by :ios do |config| config.device_tokens = ->(recipient) { recipient.notification_tokens.where(platform: :iOS).pluck(:token) } - config.json = ->(apn) { + config.format = ->(apn) { apn.alert = "Hello world" apn.custom_payload = {url: root_url(host: "example.org")} } @@ -36,7 +36,7 @@ end ## Options -* `json` +* `format` Customize the Apnotic notification object diff --git a/lib/noticed/delivery_methods/ios.rb b/lib/noticed/delivery_methods/ios.rb index 8d442898..411b97d2 100644 --- a/lib/noticed/delivery_methods/ios.rb +++ b/lib/noticed/delivery_methods/ios.rb @@ -32,7 +32,7 @@ def deliver def format_notification(apn) apn.topic = evaluate_option(:bundle_identifier) - if (method = config[:json]) + if (method = config[:format]) method = event.send(method, apn) if method.is_a?(Symbol) && event.respond_to?(method) notification.instance_exec(apn, &method) elsif notification.params.try(:has_key?, :message) diff --git a/test/delivery_methods/ios_test.rb b/test/delivery_methods/ios_test.rb index 7c19cb1d..34927364 100644 --- a/test/delivery_methods/ios_test.rb +++ b/test/delivery_methods/ios_test.rb @@ -43,7 +43,7 @@ def ok? team_id: "team_id", apns_key: "apns_key", device_tokens: [:a, :b], - json: ->(apn) { + format: ->(apn) { apn.alert = "Hello world" apn.custom_payload = {url: root_url(host: "example.org")} }, From b1c38edbcd1d872670cbea31ab21486e6c65aaa7 Mon Sep 17 00:00:00 2001 From: Phil Reynolds Date: Tue, 23 Jan 2024 21:47:19 +0000 Subject: [PATCH 8/8] Update README.md to show a mixture of methods, lambdas, and values. Co-authored-by: Chris Oliver --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0096f328..a7b0d28e 100644 --- a/README.md +++ b/README.md @@ -133,9 +133,9 @@ class CommentNotifier < Noticed::Event config.apns_key = :ios_cert config.key_id = :ios_key_id config.team_id = :ios_team_id - config.bundle_identifier = :ios_bundle_id + config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_identifier) config.device_tokens = :ios_device_tokens - config.if = :send_ios_notification? + config.if = ->(notification) { recipient.send_ios_notification? } end def ios_format(apn)