Current section
Files
Jump to
Current section
Files
lib/wallet_passes.ex
defmodule WalletPasses do
@moduledoc """
Apple Wallet and Google Wallet pass generation, management, and remote updates.
This module provides convenience functions that orchestrate both platforms.
For platform-specific control, use `WalletPasses.Apple.Builder`,
`WalletPasses.Google.Api`, etc. directly.
"""
alias WalletPasses.Apple
alias WalletPasses.Google
alias WalletPasses.PassData
alias WalletPasses.PassType
alias WalletPasses.Schema
@doc """
Generates an Apple .pkpass binary for a pass.
Creates or retrieves the Apple pass record, then builds the .pkpass bundle.
"""
def build_apple_pass(%PassData{} = pass_data, %Apple.Visual{} = visual) do
with {:ok, apple_pass} <- Schema.get_or_create_apple_pass(pass_data.serial_number) do
Apple.Builder.build_pkpass(pass_data, visual, apple_pass.auth_token)
end
end
@doc """
Generates a "Save to Google Wallet" URL for a pass.
Creates or retrieves the Google pass record, creates/updates the object
on Google's servers, and returns the save URL.
## Options
* `:class_id` — Class ID suffix to use for the object (defaults to the
pass type's standard suffix).
* `:class_config` — Map of class configuration. When provided, ensures
the class exists on Google's servers (idempotent — only created once
per VM lifetime per class). The map's `:id` key defaults to the
`:class_id` opt or to the pass type's standard suffix.
* `:origins` — Passed through to `Google.SaveUrl`.
"""
def google_save_url(%PassData{} = pass_data, %Google.Visual{} = visual, opts \\ []) do
{class_id, opts} = Keyword.pop(opts, :class_id)
{class_config, save_opts} = Keyword.pop(opts, :class_config)
with :ok <- maybe_ensure_class(class_config, class_id, pass_data.pass_type),
{:ok, google_pass} <- Schema.get_or_create_google_pass(pass_data.serial_number),
{:ok, object_id} <- Google.Api.create_object(pass_data, visual, class_id: class_id) do
Schema.update_google_object_id(google_pass, object_id)
pass_object = Google.Api.build_pass_object(pass_data, visual, class_id: class_id)
Google.SaveUrl.url(pass_object, save_opts)
end
end
@doc "Sends Apple push notifications for a pass to trigger refresh on devices."
def notify_apple_devices(serial_number) when is_binary(serial_number) do
tokens = Schema.list_push_tokens_for_serial(serial_number)
Apple.Push.notify_devices(tokens)
end
@doc """
Updates a Google Wallet pass object on Google's servers.
## Options
* `:class_id` — Pass-through to `Google.Api.update_object`.
* `:class_config` — Map of class configuration. When provided, ensures
the class exists on Google's servers (idempotent).
"""
def update_google_pass(%PassData{} = pass_data, %Google.Visual{} = visual, opts \\ []) do
{class_id, opts} = Keyword.pop(opts, :class_id)
{class_config, update_opts} = Keyword.pop(opts, :class_config)
with :ok <- maybe_ensure_class(class_config, class_id, pass_data.pass_type) do
case Schema.get_google_pass(pass_data.serial_number) do
nil ->
{:error, :not_found}
google_pass when is_nil(google_pass.object_id) ->
{:error, :no_object_id}
google_pass ->
Google.Api.update_object(
pass_data,
visual,
google_pass.object_id,
Keyword.put(update_opts, :class_id, class_id)
)
end
end
end
@doc """
Returns wallet presence for a pass across both platforms.
* `:apple` — `true` when the pass has at least one device registration.
Note: this is a "reachable for push" signal, not a "user has the pass"
signal — see `WalletPasses.EventHandler.on_pass_removed/3` for the
caveats around iOS push-token rotation and app uninstall.
* `:google` — `true` when the latest recorded callback for the pass is
`save`, `false` if `del`, and `nil` if no callback has been received
(either the pass was never saved, or the consumer doesn't have
`:google_callback_url` configured).
"""
@spec wallet_presence(String.t()) :: %{apple: boolean(), google: boolean() | nil}
def wallet_presence(serial_number) when is_binary(serial_number) do
%{
apple: Schema.has_device_registrations?(serial_number),
google: google_wallet_presence(serial_number),
}
end
defp google_wallet_presence(serial_number) do
case Schema.latest_google_callback(serial_number) do
nil -> nil
%{event_type: "save"} -> true
%{event_type: "del"} -> false
end
end
defp maybe_ensure_class(nil, _class_id, _pass_type), do: :ok
defp maybe_ensure_class(class_config, class_id, pass_type) when is_map(class_config) do
default_id = class_id || PassType.google_class_suffix(pass_type)
config = Map.put_new(class_config, :id, default_id)
Google.Api.ensure_class(config, pass_type)
end
end