Current section
Files
Jump to
Current section
Files
lib/wallet_passes/preview/components.ex
if Code.ensure_loaded?(Phoenix.LiveView) do
defmodule WalletPasses.Preview.Components do
@moduledoc """
LiveView function components for previewing wallet passes.
Optional — requires `phoenix_live_view` dependency.
## Usage
import WalletPasses.Preview.Components
<.apple_pass_preview pass_json={@apple_json} barcode_svg={@barcode_svg} barcode_format={@barcode_format}>
<:logo><img src="/images/logo.svg" class="h-7 w-auto" alt="" /></:logo>
<:strip><img src="/images/strip.png" class="w-full h-auto" alt="" /></:strip>
<:thumbnail><img src="/images/role.png" class="h-10 w-10" alt="" /></:thumbnail>
</.apple_pass_preview>
<%!-- Google auto-renders logo/hero/role images from the pass object; slots override them --%>
<.google_pass_preview pass_object={@google_obj} barcode_svg={@barcode_svg} barcode_format={@barcode_format} />
"""
use Phoenix.Component
attr(:pass_json, :map, required: true)
attr(:barcode_svg, :string, required: true)
attr(:barcode_format, :atom, default: :qr)
slot(:logo, doc: "Optional logo image rendered beside logoText in the header.")
slot(:strip, doc: "Optional full-width strip image between header and primary field.")
slot(:thumbnail, doc: "Optional image rendered beside the primary field value.")
def apple_pass_preview(assigns) do
{pass_type, ticket} = detect_apple_pass_type(assigns.pass_json)
assigns =
assigns
|> assign(:pass_type, pass_type)
|> assign(:primary, List.first(ticket["primaryFields"] || []))
|> assign(:secondary, ticket["secondaryFields"] || [])
|> assign(:auxiliary, ticket["auxiliaryFields"] || [])
|> assign(:back_fields, ticket["backFields"] || [])
|> assign(:header_fields, ticket["headerFields"] || [])
|> assign(:transit_type, ticket["transitType"])
|> assign(:bg_color, assigns.pass_json["backgroundColor"] || "rgb(255, 255, 255)")
|> assign(:fg_color, assigns.pass_json["foregroundColor"] || "rgb(38, 38, 38)")
|> assign(:label_color, assigns.pass_json["labelColor"] || "rgb(0, 168, 80)")
|> assign(:safe_barcode_svg, {:safe, assigns.barcode_svg})
~H"""
<div
class="rounded-2xl overflow-hidden shadow-xl w-full max-w-sm mx-auto border border-base-content/20"
style={"background: #{@bg_color}; color: #{@fg_color};"}
>
<div class="px-5 pt-5 pb-3">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<%= if @logo != [] do %>
{render_slot(@logo)}
<% end %>
<div class="text-sm font-semibold">{@pass_json["logoText"]}</div>
</div>
<div class="text-xs opacity-50">{@pass_json["description"]}</div>
</div>
</div>
<%= if @strip != [] do %>
<div class="w-full">{render_slot(@strip)}</div>
<% end %>
<div class="px-5 pt-3 pb-3 flex items-center justify-between">
<div class="text-2xl font-bold tracking-tight">{@primary && @primary["value"]}</div>
<%= if @thumbnail != [] do %>
{render_slot(@thumbnail)}
<% end %>
</div>
<div class="px-5 pb-3 flex gap-6">
<div :for={field <- @secondary} class="flex-1">
<div
class="text-[10px] uppercase tracking-wider font-semibold"
style={"color: #{@label_color};"}
>
{field["label"]}
</div>
<div class="text-sm font-medium">{field["value"]}</div>
</div>
</div>
<div class="px-5 pb-4 flex gap-6">
<div :for={field <- @auxiliary} class="flex-1">
<div
class="text-[10px] uppercase tracking-wider font-semibold"
style={"color: #{@label_color};"}
>
{field["label"]}
</div>
<div class="text-sm">{field["value"]}</div>
</div>
</div>
<div class="border-t border-current/10 mx-5" />
<%= if @barcode_format == :code_128 do %>
<div class="px-5 pt-4 pb-3">
<div class={"bg-white rounded p-3 [&>svg]:w-full [&>svg]:h-16"}>
{@safe_barcode_svg}
</div>
</div>
<% else %>
<div class="flex justify-center pt-4 pb-3">
<div class={"[&>svg]:w-32 [&>svg]:h-32"}>
{@safe_barcode_svg}
</div>
</div>
<% end %>
<div class="text-center pb-5">
<div class="text-xs font-mono opacity-50">{@pass_json["barcode"]["message"]}</div>
</div>
</div>
<%= if @back_fields != [] do %>
<div class="rounded-xl border border-base-content/20 bg-base-100 p-4 max-w-sm mx-auto mt-3">
<div class="text-xs font-semibold text-base-content/40 mb-2 uppercase tracking-wider">
Back of pass
</div>
<div :for={field <- @back_fields} class="py-1.5">
<div class="text-xs text-base-content/50">{field["label"]}</div>
<div class="text-sm font-mono">{field["value"]}</div>
</div>
</div>
<% end %>
"""
end
defp detect_apple_pass_type(pass_json) do
cond do
pass_json["boardingPass"] -> {:boarding_pass, pass_json["boardingPass"]}
pass_json["storeCard"] -> {:store_card, pass_json["storeCard"]}
pass_json["coupon"] -> {:coupon, pass_json["coupon"]}
pass_json["generic"] -> {:generic, pass_json["generic"]}
true -> {:event_ticket, pass_json["eventTicket"] || %{}}
end
end
attr(:pass_object, :map, required: true)
attr(:barcode_svg, :string, required: true)
attr(:barcode_format, :atom, default: :qr)
slot(:logo, doc: "Overrides the logo auto-rendered from the pass object.")
slot(:hero, doc: "Overrides the hero image auto-rendered from the pass object.")
slot(:thumbnail,
doc: "Overrides the role/thumbnail image auto-rendered from the pass object."
)
def google_pass_preview(assigns) do
holder_name =
assigns.pass_object["ticketHolderName"] ||
assigns.pass_object["passengerName"] ||
assigns.pass_object["accountName"] ||
get_in(assigns.pass_object, ["header", "defaultValue", "value"])
type_label =
get_in(assigns.pass_object, ["ticketType", "defaultValue", "value"]) ||
detect_google_type_label(assigns.pass_object)
thumb_uri =
case assigns.pass_object["imageModulesData"] do
[%{"mainImage" => %{"sourceUri" => %{"uri" => uri}}} | _] -> uri
_ -> nil
end
assigns =
assigns
|> assign(:header_value, get_in(assigns.pass_object, ["header", "defaultValue", "value"]))
|> assign(:type_label, type_label)
|> assign(:holder_name, holder_name)
|> assign(:text_modules, assigns.pass_object["textModulesData"] || [])
|> assign(:bg_color, assigns.pass_object["hexBackgroundColor"] || "#FFFFFF")
|> assign(:text_color, contrasting_text_color(assigns.pass_object["hexBackgroundColor"]))
|> assign(:logo_uri, get_in(assigns.pass_object, ["logo", "sourceUri", "uri"]))
|> assign(:hero_uri, get_in(assigns.pass_object, ["heroImage", "sourceUri", "uri"]))
|> assign(:thumb_uri, thumb_uri)
|> assign(:safe_barcode_svg, {:safe, assigns.barcode_svg})
~H"""
<div
class="rounded-2xl overflow-hidden shadow-xl w-full max-w-sm mx-auto border border-base-content/20"
style={"background: #{@bg_color}; color: #{@text_color};"}
>
<div class="px-5 pt-5 pb-3">
<div class="flex items-center gap-2">
<%= if @logo != [] do %>
{render_slot(@logo)}
<% else %>
<%= if @logo_uri do %>
<img src={@logo_uri} class="h-7 w-auto" alt="" />
<% end %>
<% end %>
<div>
<div class="text-sm font-semibold">{@header_value}</div>
<div class="text-xs opacity-50">{@type_label}</div>
</div>
</div>
</div>
<%= if @hero != [] do %>
{render_slot(@hero)}
<% else %>
<%= if @hero_uri do %>
<img src={@hero_uri} class="w-full h-auto" alt="" />
<% end %>
<% end %>
<div class="border-t border-current/10 mx-5" />
<div class="px-5 pt-3 pb-3 flex items-center justify-between">
<div class="text-2xl font-bold tracking-tight">{@holder_name}</div>
<%= if @thumbnail != [] do %>
{render_slot(@thumbnail)}
<% else %>
<%= if @thumb_uri do %>
<img src={@thumb_uri} class="h-10 w-10 opacity-80" alt="" />
<% end %>
<% end %>
</div>
<div class="px-5 py-3 space-y-3">
<div :for={mod <- @text_modules} class="flex justify-between items-baseline">
<div class="text-[10px] uppercase tracking-wider font-semibold opacity-60">
{mod["header"]}
</div>
<div class="text-sm font-medium">{mod["body"]}</div>
</div>
</div>
<div class="border-t border-current/10 mx-5" />
<%= if @barcode_format == :code_128 do %>
<div class="px-5 pt-4 pb-3">
<div class={"bg-white rounded p-3 [&>svg]:w-full [&>svg]:h-16"}>
{@safe_barcode_svg}
</div>
</div>
<% else %>
<div class="flex justify-center pt-4 pb-3">
<div class={"[&>svg]:w-32 [&>svg]:h-32"}>
{@safe_barcode_svg}
</div>
</div>
<% end %>
<div class="text-center pb-5">
<div class="text-xs font-mono opacity-50">{@pass_object["barcode"]["value"]}</div>
</div>
</div>
"""
end
defp detect_google_type_label(pass_object) do
cond do
pass_object["ticketHolderName"] -> "Event Ticket"
pass_object["passengerName"] -> "Boarding Pass"
pass_object["accountName"] -> "Loyalty Card"
true -> "Pass"
end
end
defp contrasting_text_color(nil), do: "#1a1a1a"
defp contrasting_text_color(hex) do
clean = String.trim_leading(hex, "#")
with 6 <- String.length(clean),
{r, ""} <- Integer.parse(String.slice(clean, 0, 2), 16),
{g, ""} <- Integer.parse(String.slice(clean, 2, 2), 16),
{b, ""} <- Integer.parse(String.slice(clean, 4, 2), 16) do
brightness = div(r * 299 + g * 587 + b * 114, 1000)
if brightness < 128, do: "#ffffff", else: "#1a1a1a"
else
_ -> "#1a1a1a"
end
end
end
end