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