Pigeon is the most popular Elixir push notification library for iOS and Android. This is Part 1 in a series of tips on using the package more effectively, written by its author, Henry Popp.

Asynchronous Pushing

By default all pushes in Pigeon are synchronous. While this works great for testing, you should almost always be using async pushing in production. Pigeon is certainly fast, but dispatching 10,000+ push notifications at once can still take a few seconds.

It’s easy to set up with the :on_response key. Define a function that takes a single parameter. Your notification will be returned with updated information about errors or updates that you may want to handle.

APNS

def send_push do
  n = Pigeon.APNS.Notification.new("message", "device token", "push topic")
  Pigeon.APNS.push(n, on_response: &handle_push/1)
end

def handle_push(%APNS.Notification{response: :success}) do
 # success! probably don't need to do anything
end

def handle_push(%APNS.Notification{response: :bad_device_token}) do
  # bad token! remove it from the database! 
end

def handle_push(%APNS.Notification{response: _other_error}) do
  # some other error happened?
end

FCM

FCM is a little more complicated, but still pretty straightforward. Pushes can be sent in batches of 1,000, so the responses must be iterated individually.

def send_push do
  data = %{message: "your message"}
  n = Pigeon.FCM.Notification.new(["regid_1", "regid_2"], data)
  Pigeon.FCM.push(n, on_response: &handle_push/1)
end

def handle_push(%FCM.Notification{status: :success} = n) do
  # success! let's check each reg ID
  for response <- n.response, do: handle_regid(response)
end

def handle_push(%FCM.Notification{status: _error} = n) do
  # entire push batch failed!
end

def handle_regid({:success, regid}) do
  # success!
end

def handle_regid({:update, {old_regid, new_regid}) do
  # replace the regid in the database!
end

def handle_regid({:invalid_registration, regid}) do
  # remove it!
end

def handle_regid({:not_registered, regid}) do
  # remove it!
end

def handle_regid({_error, regid}) do
  # some other error happened
end

Don’t like that many function handlers? FCM.Notification provides shortcut helpers to get the regids you’re looking for.

def send_push do
  data = %{message: "your message"}
  n = Pigeon.FCM.Notification.new(["regid_1", "regid_2"], data)
  Pigeon.FCM.push(n, on_response: handle_push/1)
end

def handle_push(%FCM.Notification{status: :success} = notif) do
  to_update = FCM.Notification.update?(notif)
  to_remove = FCM.Notification.remove?(notif)
  # do the reg ID update and deletes
end

def handle_push(%FCM.Notification{status: _other_error}) do
  # some other error happened
end

ADM

ADM is an interesting hybrid of APNS and FCM behavior. Only one ADM push can be sent at a time, but the responses are identical to FCM.

def send_push do
  data = %{ message: "your message" }
  n = Pigeon.ADM.Notification.new("your registration id", data)
  Pigeon.ADM.push(n, on_response: &handle_push/1)
end

def handle_push(%ADM.Notification{response: :success}) do
  # success!
end

def handle_push(%ADM.Notification{response: :update} = n) do
  new_regid = n.update_registration_id
  # update it!
end

def handle_push(%ADM.Notification{response: :invalid_registration_id}) do
  # remove it! 
end

def handle_push(%ADM.Notification{response: :unregistered}) do
  # remove it! 
end

def handle_push(%ADM.Notification{response: _other_error}) do
  # some other error happened?
end

Conclusion

Pigeon’s async API is a bit different from traditional module callbacks, but the design was intentional. Data on the notification struct dictates what the callback function should do, instead of creating separate functions for each type of response. It’s a much cleaner approach that takes full advantage of Elixir pattern matching, leading to far more readable code.

Interested in learning more about Pigeon? Check it out on GitHub!