README.md

# 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.