Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS Delivery Method Updates and a couple of docs enhancements #377

Merged
merged 9 commits into from
Jan 23, 2024
120 changes: 110 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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?
phil-6 marked this conversation as resolved.
Show resolved Hide resolved
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:

Expand All @@ -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:

Expand All @@ -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.

Expand All @@ -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:

Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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:

Expand Down
75 changes: 75 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/delivery_methods/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
phil-6 marked this conversation as resolved.
Show resolved Hide resolved
config.json = ->(apn) {
apn.alert = "Hello world"
apn.custom_payload = {url: root_url(host: "example.org")}
}
Expand Down
9 changes: 5 additions & 4 deletions lib/noticed/delivery_methods/ios.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def deliver
def format_notification(apn)
apn.topic = evaluate_option(:bundle_identifier)

if (method = config[:format])
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]
Expand Down Expand Up @@ -70,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

Expand Down
2 changes: 1 addition & 1 deletion test/delivery_methods/ios_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
},
Expand Down