WalletPasses
Apple Wallet and Google Wallet pass generation, management, and remote updates for Elixir.
Features
- Apple Wallet: Build signed
.pkpassbundles, 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:
PassDatastruct for content, separateApple.Visual/Google.Visualfor 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.3"},
]
endGenerate and run the database migrations:
$ mix wallet_passes.gen.migration
$ mix ecto.migrateConfiguration
# 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
end2. 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.Router4. 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")NFC Passes
Apple Wallet (VAS Protocol)
Add NFC fields to your PassData to enable tap-to-identify:
pass_data = PassData.new(
serial_number: "MEMBER-001",
nfc_message: "member-id:MEMBER-001",
nfc_encryption_public_key: "MDkwEwYH...", # Base64 X.509 ECDH P-256 public key
nfc_requires_authentication: false,
# ... other fields
)
Both nfc_message and nfc_encryption_public_key are required -- if either is nil, the NFC dictionary is omitted from the pass.
Note: Apple NFC passes require a special entitlement from Apple. Apply at developer.apple.com/contact/passkit.
Google Wallet (Smart Tap)
Set nfc_message on the pass data (used as the Smart Tap redemption value), and enable Smart Tap on the class:
pass_data = PassData.new(
serial_number: "MEMBER-001",
nfc_message: "REDEEM-MEMBER-001",
# ... other fields
)
# When creating the class, enable Smart Tap:
WalletPasses.Google.Api.create_or_update_class(%{
id: "loyalty_class",
issuer_name: "My Store",
event_name: "Loyalty Card",
enable_smart_tap: true,
redemption_issuers: ["YOUR_REDEMPTION_ISSUER_ID"],
})Note: Google Smart Tap requires partner approval. Contact Google Wallet support to enable Smart Tap for your issuer account.
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 (ex_passbook) package is the only other Elixir library for .pkpass generation. However:
- Missing
authenticationTokensupport --passbookdoesn't support theauthenticationTokenfield, which is required for the pass update lifecycle (Apple devices use it to authenticate callback requests) - URL camelization bug --
passbookhas 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
.pkpassbuilding - No runtime dependency on OpenSSL -- This library uses a pure Erlang PKCS#7 implementation for
.pkpasssigning, whilepassbookshells out toopenssl smime
System Requirements
- PostgreSQL -- required for pass persistence (via Ecto)
License
MIT -- see LICENSE for details.