grocer interfaces with the Apple Push Notification Service to send push notifications to iOS devices.
There are other gems out there to do this, but grocer plans to be the cleanest, most extensible, and friendliest.
- Ruby/MRI 2.1, 2.0, 1.9.x, JRuby 1.7.x in 1.9 mode, Rubinius in 1.9 mode
Add this line to your application's Gemfile:
gem 'grocer'
If you are using JRuby, you will also need to add this to enable full OpenSSL support:
gem 'jruby-openssl'
# `certificate` is the only required option; the rest will default to the values
# shown here.
#
# Information on obtaining a `.pem` file for use with `certificate` is shown
# later.
pusher = Grocer.pusher(
certificate: "/path/to/cert.pem", # required
passphrase: "", # optional
gateway: "gateway.push.apple.com", # optional; See note below.
port: 2195, # optional
retries: 3 # optional
)
certificate
: If you don't have the certificate stored in a file, you can pass any object that responds toread
. Example:certificate: StringIO.new(pem_string)
gateway
: Defaults to different values depending on theRAILS_ENV
orRACK_ENV
environment variables. If set toproduction
, defaults togateway.push.apple.com
, if set totest
, defaults tolocalhost
(see Acceptance Testing later), otherwise defaults togateway.sandbox.push.apple.com
.retries
: The number of times grocer will retry writing to or reading from the Apple Push Notification Service before raising any errors to client code.
# `device_token` and either `alert` or `badge` are required.
#
# Information on obtaining `device_token` is shown later.
notification = Grocer::Notification.new(
device_token: "fe15a27d5df3c34778defb1f4f3880265cc52c0c047682223be59fb68500a9a2",
alert: "Hello from Grocer!",
badge: 42,
category: "a category", # optional; used for custom notification actions
sound: "siren.aiff", # optional
expiry: Time.now + 60*60, # optional; 0 is default, meaning the message is not stored
identifier: 1234, # optional; must be an integer
content_available: true # optional; any truthy value will set 'content-available' to 1
)
pusher.push(notification)
It is desirable to reuse the same connection to send multiple notifications, as is recommended by Apple.
pusher = Grocer.pusher(connection_options)
notifications.each do |notification|
pusher.push(notification)
end
The Apple documentation says "Providers can specify custom payload values
outside the Apple-reserved aps namespace." To specify a custom payload, set
Grocer::Notification#custom
.
notification = Grocer::Notification.new(
device_token: "...",
alert: "Hello from Grocer",
custom: {
"acme2": ["bang", "whiz"]
}
)
# Generates a JSON payload like:
# {"aps": {"alert": "Hello from Grocer"}, "acme2": ["bang", "whiz"]}
A Grocer::PassbookNotification
is a specialized kind of notification which
does not require any payload. That is, you need not (and Apple explicitly says
not to)
send any payload for a Passbook notification. If you do, it will be ignored.
notification = Grocer::PassbookNotification.new(device_token: "...")
# Generates a JSON payload like:
# {"aps": {}}
Grocer also supports the special Newsstand 'content-available' notification.
Grocer::NewsstandNotification
can be used for this. Like
Grocer::PassbookNotification
, it is a specialized kind of notification which
does not require any payload. Likewise, anything you add to it will be ignored.
notification = Grocer::NewsstandNotification.new(device_token: "...")
# Generates a JSON payload like:
# {"aps": {"content-available":1}}
Grocer can be used for Safari Push Notifications introduced in Mavericks.
notification = Grocer::SafariNotification.new(
device_token: '...', # required
title: 'Hello from Grocer', # required
body: 'Hi', # required
action: 'Read', # optional; the label of the action button
url_args: ['arg1'] # required (array); values that are paired with the placeholders inside the urlFormatString.
# Apple's documention lists url-args as optional, but in testing it was found to be required
)
Generates a JSON payload like:
{
"aps": {
"alert": {
"title": "Hello from Grocer",
"body": "Hi",
"action": "Read"
},
"url-args": [ "arg1" ]
}
}
# `certificate` is the only required option; the rest will default to the values
# shown here.
feedback = Grocer.feedback(
certificate: "/path/to/cert.pem", # required
passphrase: "", # optional
gateway: "feedback.push.apple.com", # optional; See note below.
port: 2196, # optional
retries: 3 # optional
)
feedback.each do |attempt|
puts "Device #{attempt.device_token} failed at #{attempt.timestamp}"
end
gateway
: Defaults tofeedback.push.apple.com
only when running in a production environment, as determined by either theRAILS_ENV
orRACK_ENV
environment variables. In all other cases, it defaults to the sandbox gateway,feedback.sandbox.push.apple.com
.retries
: The number of times grocer will retry writing to or reading from the Apple Push Notification Service before raising any errors to client code.
Grocer ships with framework to setup a real looking APNS server. It listens on a real SSL-capable socket bound to localhost.
You can setup an APNS client to talk to it, then inspect the notifications the server received.
The server simply exposes a blocking queue where notifications are placed when they are received. It is your responsibility to timeout if a message is not received in a reasonable amount of time.
For example, in RSpec:
require 'timeout'
describe "apple push notifications" do
before do
@server = Grocer.server(port: 2195)
@server.accept # starts listening in background
end
after do
@server.close
end
specify "As a user, I receive notifications on my phone when awesome things happen" do
# ... exercise code that would send APNS notifications ...
Timeout.timeout(3) {
notification = @server.notifications.pop # blocking
expect(notification.alert).to eq("An awesome thing happened")
}
end
end
A device token is obtained from within the iOS app. More details are in Apple's Registering for Remote Notifications documentation.
The key code for this purpose is:
- (void)applicationDidFinishLaunching:(UIApplication *)app {
// other setup tasks here....
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
}
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
NSLog(@"Got device token: %@", [devToken description]);
[self sendProviderDeviceToken:[devToken bytes]]; // custom method; e.g., send to a web service and store
}
- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
NSLog(@"Error in registration. Error: %@", err);
}
Login to the iOS Provisioning Portal (App IDs).
Configure the appropriate certificate for push notifications and download the certificate:
Open the file in Keychain Access, then expand the certificate to show both the certificate and the private key. Command select so both are highlighted:
Control click and select to export the 2 items:
Save the items as a .p12
file. Open a terminal window and run the following
command:
openssl pkcs12 -in exported_certificate.p12 -out certificate.pem -nodes -clcerts
The certificate.pem
file that is generated can be used with grocer.
GitHub Issues and Pull
Requests are the primary venues for
communicating issues and discussing possible features. Several of us also
regularly hang out in the #grocer
channel on Freenode; feel free to pop in
and ask questions there as well. Thanks! ❤️