# WalletPasses
Apple Wallet and Google Wallet pass generation, management, and remote updates for Elixir.
## Features
- **Apple Wallet:** Build signed `.pkpass` bundles, handle device registration callbacks, send silent APNs pushes
- **Google Wallet:** Create/update event ticket objects and classes, generate "Save to Google Wallet" URLs
- **Platform-agnostic data model:** `PassData` struct for content, separate `Apple.Visual` / `Google.Visual` for platform-specific styling
- **Theme helper:** Convert shared colors into platform-specific visual configs
- **QR code generation:** SVG and PNG output
- **Ecto persistence:** Separate per-platform tables with migration generator
- **Optional add-ons:** LiveView preview components, Oban background sync worker
## Installation
Add `wallet_passes` to your list of dependencies in `mix.exs`:
def deps do
[
{:wallet_passes, "~> 0.1.0"},
]
end
Generate and run the database migrations:
$ mix wallet_passes.gen.migration
$ mix ecto.migrate
## Configuration
# config/config.exs
config :wallet_passes,
repo: MyApp.Repo,
pass_data_provider: MyApp.WalletPassProvider,
apple_pass_type_id: "pass.com.example.mypass",
apple_web_service_url: "https://yourdomain.com/passes/apple"
# config/runtime.exs
config :wallet_passes,
apple_team_id: System.get_env("APPLE_TEAM_ID"),
apple_pass_type_cert: System.get_env("APPLE_PASS_TYPE_CERT"),
apple_pass_type_key: System.get_env("APPLE_PASS_TYPE_KEY"),
apple_wwdr_cert: System.get_env("APPLE_WWDR_CERT"),
google_issuer_id: System.get_env("GOOGLE_WALLET_ISSUER_ID"),
google_service_account_json: System.get_env("GOOGLE_WALLET_SERVICE_ACCOUNT_JSON")
Certificate/key values accept file paths, PEM strings, or base64-encoded values.
## Quick Start
### 1. Implement the PassDataProvider
The library needs to look up pass data autonomously (e.g., when Apple requests an updated pass). Implement the behaviour:
defmodule MyApp.WalletPassProvider do
@behaviour WalletPasses.PassDataProvider
@impl true
def build_pass_data(serial_number) do
case MyApp.find_by_serial(serial_number) do
nil -> {:error, :not_found}
record ->
{:ok, %{
pass_data: %WalletPasses.PassData{
serial_number: serial_number,
event_name: record.event_name,
holder_name: record.holder_name,
primary_fields: [{"name", "Name", record.holder_name}],
# ... more fields
},
apple: %WalletPasses.Apple.Visual{
background_color: "#1A1A1A",
foreground_color: "#FFFFFF",
label_color: "#D4A843",
icon_path: "/path/to/icon.png",
},
google: %WalletPasses.Google.Visual{
background_color: "#1A1A1A",
logo_uri: "https://example.com/logo.png",
},
}}
end
end
end
### 2. Generate passes
# Build an Apple .pkpass
{:ok, pkpass_binary} = WalletPasses.build_apple_pass(pass_data, apple_visual)
# Get a Google Wallet save URL
{:ok, url} = WalletPasses.google_save_url(pass_data, google_visual)
### 3. Mount the Apple callback router
Apple devices call back to your server to register for updates and fetch the latest pass. Mount the router in your Phoenix app:
# router.ex
forward "/passes/apple", WalletPasses.Apple.Router
### 4. Send push updates
WalletPasses.notify_apple_devices("SERIAL-NUMBER")
## Theme Helper
Use the `Theme` struct to share colors across platforms:
theme = %WalletPasses.Theme{
background_color: "#1A1A1A",
foreground_color: "#FFFFFF",
label_color: "#D4A843",
logo_text: "My Event",
}
apple_visual = theme
|> WalletPasses.Theme.to_apple_visual()
|> struct!(icon_path: "/path/to/icon.png", strip_image_path: "/path/to/strip.png")
google_visual = theme
|> WalletPasses.Theme.to_google_visual()
|> struct!(logo_uri: "https://example.com/logo.png", hero_image_uri: "https://example.com/hero.png")
## Optional Add-ons
### Preview Components (Phoenix LiveView)
Add `{:phoenix_live_view, "~> 1.0"}` to your deps, then:
import WalletPasses.Preview.Components
<.apple_pass_preview pass_json={@apple_json} qr_svg={@qr_svg} />
<.google_pass_preview pass_object={@google_obj} qr_svg={@qr_svg} />
### Background Sync (Oban)
Add `{:oban, "~> 2.18"}` to your deps, then:
# Sync specific passes
WalletPasses.Sync.sync(["SERIAL-1", "SERIAL-2"])
# Sync all passes in the database
WalletPasses.Sync.sync_all()
## Why not passbook?
The [`passbook`](https://hex.pm/packages/passbook) (ex_passbook) package is the only other Elixir library for `.pkpass` generation. Both it and this library shell out to `openssl smime` for PKCS#7 signing -- the signing approach is equivalent. However:
- **Missing `authenticationToken` support** -- `passbook` doesn't support the `authenticationToken` field, which is required for the pass update lifecycle (Apple devices use it to authenticate callback requests)
- **URL camelization bug** -- `passbook` has a known bug that mis-cases fields containing "url"
- **Full lifecycle** -- This library owns the entire pass lifecycle (generation, callbacks, push updates, Google Wallet API) rather than just `.pkpass` building
Pure-Erlang signing (eliminating the OpenSSL dependency) is a future goal. Contributing fixes upstream to `passbook` is also under consideration.
## System Requirements
- **OpenSSL** -- required for `.pkpass` signing (used via `System.cmd/3`)
- **PostgreSQL** -- required for pass persistence (via Ecto)
## License
MIT -- see [LICENSE](LICENSE) for details.