From 2fb88df853481855bad72d7c02410bf2d4cdd789 Mon Sep 17 00:00:00 2001 From: jamey Date: Thu, 12 Feb 2026 22:55:29 +0000 Subject: [PATCH] replace setup checklist with interactive stepper 3-step vertical stepper with inline forms for Printify and Stripe, real-time sync progress via PubSub, and celebration state on go-live. Co-Authored-By: Claude Opus 4.6 --- .../sync/product_sync_worker.ex | 9 + .../live/admin/dashboard.ex | 698 ++++++++++++++++-- .../live/admin/dashboard_test.exs | 59 +- 3 files changed, 701 insertions(+), 65 deletions(-) diff --git a/lib/simpleshop_theme/sync/product_sync_worker.ex b/lib/simpleshop_theme/sync/product_sync_worker.ex index a132911..1fd925a 100644 --- a/lib/simpleshop_theme/sync/product_sync_worker.ex +++ b/lib/simpleshop_theme/sync/product_sync_worker.ex @@ -66,6 +66,7 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do defp sync_products(conn) do Logger.info("Starting product sync for #{conn.provider_type} (#{conn.id})") Products.update_sync_status(conn, "syncing") + broadcast_sync(conn.id, {:sync_status, "syncing"}) try do do_sync_products(conn) @@ -73,6 +74,7 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do e -> Logger.error("Product sync crashed for #{conn.provider_type}: #{Exception.message(e)}") Products.update_sync_status(conn, "failed") + broadcast_sync(conn.id, {:sync_status, "failed"}) {:error, :sync_crashed} end end @@ -95,11 +97,14 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do ) Products.update_sync_status(conn, "completed", DateTime.utc_now()) + product_count = Products.count_products_for_connection(conn.id) + broadcast_sync(conn.id, {:sync_status, "completed", product_count}) :ok else {:error, reason} = error -> Logger.error("Product sync failed for #{conn.provider_type}: #{inspect(reason)}") Products.update_sync_status(conn, "failed") + broadcast_sync(conn.id, {:sync_status, "failed"}) error end end @@ -146,6 +151,10 @@ defmodule SimpleshopTheme.Sync.ProductSyncWorker do Products.upsert_product(conn, attrs) end + defp broadcast_sync(conn_id, message) do + Phoenix.PubSub.broadcast(SimpleshopTheme.PubSub, "sync:#{conn_id}", message) + end + defp sync_product_associations(product, product_data) do # Sync images images = diff --git a/lib/simpleshop_theme_web/live/admin/dashboard.ex b/lib/simpleshop_theme_web/live/admin/dashboard.ex index 82a0472..71e4cdc 100644 --- a/lib/simpleshop_theme_web/live/admin/dashboard.ex +++ b/lib/simpleshop_theme_web/live/admin/dashboard.ex @@ -1,7 +1,10 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do use SimpleshopThemeWeb, :live_view - alias SimpleshopTheme.{Cart, Orders, Setup} + alias SimpleshopTheme.{Cart, Orders, Products, Settings, Setup} + alias SimpleshopTheme.Products.ProviderConnection + alias SimpleshopTheme.Providers + alias SimpleshopTheme.Stripe.Setup, as: StripeSetup @impl true def mount(_params, _session, socket) do @@ -10,15 +13,215 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do paid_count = Map.get(status_counts, "paid", 0) recent_orders = Orders.list_orders(status: "paid") |> Enum.take(5) + conn = Products.get_provider_connection_by_type("printify") + + if conn && connected?(socket) do + Phoenix.PubSub.subscribe(SimpleshopTheme.PubSub, "sync:#{conn.id}") + end + + active_step = determine_active_step(status) + {:ok, socket |> assign(:page_title, "Dashboard") |> assign(:setup, status) + |> assign(:active_step, active_step) + # Printify state + |> assign(:printify_conn, conn) + |> assign(:printify_form, to_form(%{"api_key" => ""}, as: :printify)) + |> assign(:printify_testing, false) + |> assign(:printify_test_result, nil) + |> assign(:printify_saving, false) + |> assign(:pending_api_key, nil) + |> assign(:sync_status, conn && conn.sync_status) + # Stripe state + |> assign(:stripe_form, to_form(%{"api_key" => ""}, as: :stripe)) + |> assign(:stripe_connecting, false) + |> assign(:stripe_api_key_hint, Settings.secret_hint("stripe_api_key")) + # Celebration + |> assign(:just_went_live, false) + # Stats |> assign(:paid_count, paid_count) |> assign(:revenue, Orders.total_revenue()) |> assign(:recent_orders, recent_orders)} end + # -- Step determination -- + + defp determine_active_step(status) do + cond do + !status.printify_connected -> :printify + !status.products_synced -> :printify + !status.stripe_connected -> :stripe + !status.site_live -> :go_live + true -> :complete + end + end + + # -- Events: Printify -- + + @impl true + def handle_event("validate_printify", %{"printify" => params}, socket) do + {:noreply, assign(socket, pending_api_key: params["api_key"])} + end + + def handle_event("test_printify", _params, socket) do + api_key = socket.assigns.pending_api_key + + if api_key in [nil, ""] do + {:noreply, assign(socket, printify_test_result: {:error, :no_api_key})} + else + socket = assign(socket, printify_testing: true, printify_test_result: nil) + + temp_conn = %ProviderConnection{ + provider_type: "printify", + api_key_encrypted: encrypt_api_key(api_key) + } + + result = Providers.test_connection(temp_conn) + + {:noreply, assign(socket, printify_testing: false, printify_test_result: result)} + end + end + + def handle_event("connect_printify", %{"printify" => %{"api_key" => api_key}}, socket) do + if api_key == "" do + {:noreply, put_flash(socket, :error, "Please enter your Printify API token")} + else + socket = assign(socket, printify_saving: true) + + params = + %{"api_key" => api_key, "provider_type" => "printify"} + |> maybe_add_shop_config(socket.assigns.printify_test_result) + |> maybe_add_name(socket.assigns.printify_test_result) + + case Products.create_provider_connection(params) do + {:ok, connection} -> + Products.enqueue_sync(connection) + + if connected?(socket) do + Phoenix.PubSub.subscribe(SimpleshopTheme.PubSub, "sync:#{connection.id}") + end + + status = %{socket.assigns.setup | printify_connected: true} + + {:noreply, + socket + |> assign(:printify_saving, false) + |> assign(:printify_conn, connection) + |> assign(:sync_status, "syncing") + |> assign(:setup, status) + |> put_flash(:info, "Connected to Printify! Syncing products...")} + + {:error, _changeset} -> + {:noreply, + socket + |> assign(:printify_saving, false) + |> put_flash(:error, "Failed to save connection")} + end + end + end + + def handle_event("retry_sync", _params, socket) do + conn = socket.assigns.printify_conn + + if conn do + Products.enqueue_sync(conn) + {:noreply, assign(socket, sync_status: "syncing")} + else + {:noreply, socket} + end + end + + # -- Events: Stripe -- + + def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do + if api_key == "" do + {:noreply, put_flash(socket, :error, "Please enter your Stripe secret key")} + else + socket = assign(socket, stripe_connecting: true) + + case StripeSetup.connect(api_key) do + {:ok, _result} -> + status = %{socket.assigns.setup | stripe_connected: true, can_go_live: true} + + {:noreply, + socket + |> assign(:stripe_connecting, false) + |> assign(:setup, status) + |> assign(:stripe_api_key_hint, Settings.secret_hint("stripe_api_key")) + |> assign(:active_step, :go_live) + |> put_flash(:info, "Stripe connected")} + + {:error, message} -> + {:noreply, + socket + |> assign(:stripe_connecting, false) + |> put_flash(:error, "Stripe connection failed: #{message}")} + end + end + end + + # -- Events: Go live -- + + def handle_event("go_live", _params, socket) do + {:ok, _} = Settings.set_site_live(true) + status = %{socket.assigns.setup | site_live: true} + + {:noreply, + socket + |> assign(:setup, status) + |> assign(:just_went_live, true)} + end + + # -- Events: Step navigation -- + + def handle_event("toggle_step", %{"step" => step}, socket) do + step = String.to_existing_atom(step) + + new_active = + if socket.assigns.active_step == step do + determine_active_step(socket.assigns.setup) + else + step + end + + {:noreply, assign(socket, active_step: new_active)} + end + + # -- PubSub: Sync progress -- + + @impl true + def handle_info({:sync_status, "completed", product_count}, socket) do + status = %{ + socket.assigns.setup + | products_synced: true, + product_count: product_count + } + + active_step = if status.stripe_connected, do: :go_live, else: :stripe + + {:noreply, + socket + |> assign(:setup, status) + |> assign(:sync_status, "completed") + |> assign(:active_step, active_step) + |> put_flash(:info, "#{product_count} products synced")} + end + + def handle_info({:sync_status, "failed"}, socket) do + {:noreply, + socket + |> assign(:sync_status, "failed") + |> put_flash(:error, "Product sync failed — try again")} + end + + def handle_info({:sync_status, status}, socket) do + {:noreply, assign(socket, sync_status: status)} + end + + # -- Render -- + @impl true def render(assigns) do ~H""" @@ -26,8 +229,24 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do Dashboard - <%!-- Setup checklist (when not fully set up) --%> - <.setup_checklist :if={!@setup.site_live} setup={@setup} /> + <%!-- Celebration state --%> + <.celebration :if={@just_went_live} /> + + <%!-- Setup stepper (when not live and not celebrating) --%> + <.setup_stepper + :if={!@setup.site_live and !@just_went_live} + setup={@setup} + active_step={@active_step} + printify_conn={@printify_conn} + printify_form={@printify_form} + printify_testing={@printify_testing} + printify_test_result={@printify_test_result} + printify_saving={@printify_saving} + sync_status={@sync_status} + stripe_form={@stripe_form} + stripe_connecting={@stripe_connecting} + stripe_api_key_hint={@stripe_api_key_hint} + /> <%!-- Stats --%>
@@ -55,14 +274,17 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do

Recent orders

- <.link navigate={~p"/admin/orders"} class="text-sm text-zinc-500 hover:text-zinc-700"> + <.link + navigate={~p"/admin/orders"} + class="text-sm text-base-content/60 hover:text-base-content" + > View all →
<%= if @recent_orders == [] do %> -
- <.icon name="hero-inbox" class="size-10 mx-auto mb-3 text-zinc-300" /> +
+ <.icon name="hero-inbox" class="size-10 mx-auto mb-3 text-base-content/30" />

No orders yet

Orders will appear here once customers check out.

@@ -70,7 +292,7 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do
- + @@ -81,12 +303,12 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do - - + + @@ -98,58 +320,385 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do """ end - # -- Function components -- + # ========================================================================== + # Setup stepper components + # ========================================================================== attr :setup, :map, required: true + attr :active_step, :atom, required: true + attr :printify_conn, :any, required: true + attr :printify_form, :any, required: true + attr :printify_testing, :boolean, required: true + attr :printify_test_result, :any, required: true + attr :printify_saving, :boolean, required: true + attr :sync_status, :string, required: true + attr :stripe_form, :any, required: true + attr :stripe_connecting, :boolean, required: true + attr :stripe_api_key_hint, :string, required: true - defp setup_checklist(assigns) do - steps = [ - %{done: assigns.setup.admin_created, label: "Create admin account", href: nil}, - %{ - done: assigns.setup.printify_connected, - label: "Connect to Printify", - href: ~p"/admin/providers/new" - }, - %{done: assigns.setup.products_synced, label: "Sync products", href: nil}, - %{done: assigns.setup.stripe_connected, label: "Connect Stripe", href: ~p"/admin/settings"}, - %{done: assigns.setup.site_live, label: "Go live", href: ~p"/admin/settings"} - ] - - done_count = Enum.count(steps, & &1.done) - assigns = assign(assigns, steps: steps, done_count: done_count, total: length(steps)) - + defp setup_stepper(assigns) do ~H""" -
-
-

Setup progress

- {@done_count}/{@total} -
-
-
+
    + <%!-- Step 1: Printify --%> + <.setup_step + step={:printify} + number={1} + title="Connect to Printify" + active_step={@active_step} + done={@setup.printify_connected and @setup.products_synced} + last={false} + next_done={@setup.stripe_connected} > -
-
-
    -
  • - <%= if step.done do %> - <.icon name="hero-check-circle" class="size-5 text-green-500 shrink-0" /> - {step.label} - <% else %> - <.icon name="hero-circle-stack" class="size-5 text-zinc-300 shrink-0" /> - <%= if step.href do %> - <.link navigate={step.href} class="text-zinc-700 hover:underline">{step.label} - <% else %> - {step.label} - <% end %> - <% end %> -
  • -
+ <:summary :if={@setup.printify_connected and @setup.products_synced}> + Connected · {@setup.product_count} products synced + + <:content> + <.printify_step_content + setup={@setup} + printify_conn={@printify_conn} + printify_form={@printify_form} + printify_testing={@printify_testing} + printify_test_result={@printify_test_result} + printify_saving={@printify_saving} + sync_status={@sync_status} + /> + + + + <%!-- Step 2: Stripe --%> + <.setup_step + step={:stripe} + number={2} + title="Connect Stripe" + active_step={@active_step} + done={@setup.stripe_connected} + last={false} + next_done={@setup.site_live} + > + <:summary :if={@setup.stripe_connected}> + Connected · {@stripe_api_key_hint} + + <:content> + <.stripe_step_content + stripe_form={@stripe_form} + stripe_connecting={@stripe_connecting} + /> + + + + <%!-- Step 3: Go live --%> + <.setup_step + step={:go_live} + number={3} + title="Go live" + active_step={@active_step} + done={@setup.site_live} + last={true} + next_done={false} + > + <:content> + <.go_live_step_content setup={@setup} /> + + +
""" end + attr :step, :atom, required: true + attr :number, :integer, required: true + attr :title, :string, required: true + attr :active_step, :atom, required: true + attr :done, :boolean, required: true + attr :last, :boolean, required: true + attr :next_done, :boolean, required: true + + slot :summary + slot :content, required: true + + defp setup_step(assigns) do + is_active = assigns.active_step == assigns.step + is_clickable = assigns.done + + assigns = + assigns + |> assign(:is_active, is_active) + |> assign(:is_clickable, is_clickable) + + ~H""" +
  • + <%!-- Connector line --%> +
  • + """ + end + + # -- Printify step content -- + + attr :setup, :map, required: true + attr :printify_conn, :any, required: true + attr :printify_form, :any, required: true + attr :printify_testing, :boolean, required: true + attr :printify_test_result, :any, required: true + attr :printify_saving, :boolean, required: true + attr :sync_status, :string, required: true + + defp printify_step_content(assigns) do + ~H""" + <%!-- Not yet connected: show form --%> +
    +

    + Connect your Printify account to import products. + Get an API token from + Printify → Account → Connections + . +

    + + <.form + for={@printify_form} + phx-change="validate_printify" + phx-submit="connect_printify" + > + <.input + field={@printify_form[:api_key]} + type="password" + label="Printify API token" + placeholder="Paste your token here" + autocomplete="off" + /> + +
    + + <.button type="submit" disabled={@printify_saving or @printify_testing}> + {if @printify_saving, do: "Connecting...", else: "Connect to Printify"} + +
    + + <.printify_test_feedback :if={@printify_test_result} result={@printify_test_result} /> + +
    + + <%!-- Connected, syncing --%> +
    + <.icon name="hero-arrow-path" class="size-4 animate-spin text-base-content/40" /> + Syncing products from Printify... +
    + + <%!-- Connected, sync failed --%> +
    +

    Product sync failed.

    + +
    + + <%!-- Connected, synced (shown when user expands a completed step) --%> +
    +

    + {@setup.product_count} products synced from Printify. +

    +
    + """ + end + + attr :result, :any, required: true + + defp printify_test_feedback(assigns) do + ~H""" +
    + <%= case @result do %> + <% {:ok, info} -> %> + + <.icon name="hero-check-circle" class="size-4" /> Connected to {info.shop_name} + + <% {:error, reason} -> %> + + <.icon name="hero-x-circle" class="size-4" /> + {format_printify_error(reason)} + + <% end %> +
    + """ + end + + # -- Stripe step content -- + + attr :stripe_form, :any, required: true + attr :stripe_connecting, :boolean, required: true + + defp stripe_step_content(assigns) do + ~H""" +
    +

    + Enter your Stripe secret key to accept payments. + Find it in your + + Stripe dashboard + + under Developers → API keys. +

    + + <.form for={@stripe_form} phx-submit="connect_stripe"> + <.input + field={@stripe_form[:api_key]} + type="password" + label="Secret key" + autocomplete="off" + placeholder="sk_test_... or sk_live_..." + /> +

    + Starts with sk_test_ or sk_live_. Encrypted at rest. +

    +
    + <.button phx-disable-with="Connecting..."> + {if @stripe_connecting, do: "Connecting...", else: "Connect Stripe"} + +
    + +
    + """ + end + + # -- Go live step content -- + + attr :setup, :map, required: true + + defp go_live_step_content(assigns) do + ~H""" +
    +

    + Your shop is ready. Visitors currently see a "coming soon" page — + hit the button to make it live. +

    + +
    + """ + end + + # -- Celebration -- + + defp celebration(assigns) do + ~H""" +
    + <.icon name="hero-check-badge" class="size-12 mx-auto text-green-600 mb-3" /> +

    Your shop is live!

    +

    + Customers can now browse and buy from your shop. +

    +
    + <.link + navigate={~p"/"} + class="inline-flex items-center justify-center gap-1.5 rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white hover:bg-green-500" + > + <.icon name="hero-arrow-top-right-on-square-mini" class="size-4" /> View your shop + + <.link + navigate={~p"/admin/theme"} + class="inline-flex items-center justify-center gap-1.5 rounded-md bg-base-100 px-3 py-2 text-sm font-medium text-base-content ring-1 ring-base-300 ring-inset hover:bg-base-200/50" + > + <.icon name="hero-paint-brush-mini" class="size-4" /> Customise theme + +
    +
    + """ + end + + # ========================================================================== + # Stats components + # ========================================================================== + attr :label, :string, required: true attr :value, :any, required: true attr :icon, :string, required: true @@ -159,15 +708,15 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do ~H""" <.link navigate={@href} - class="rounded-lg border border-zinc-200 p-4 hover:border-zinc-300 transition-colors" + class="rounded-lg border border-base-200 p-4 hover:border-base-300 transition-colors" >
    -
    - <.icon name={@icon} class="size-5 text-zinc-600" /> +
    + <.icon name={@icon} class="size-5 text-base-content/60" />

    {@value}

    -

    {@label}

    +

    {@label}

    @@ -177,13 +726,13 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do defp fulfilment_pill(assigns) do {color, label} = case assigns.status do - "unfulfilled" -> {"bg-zinc-100 text-zinc-600", "unfulfilled"} + "unfulfilled" -> {"bg-base-200 text-base-content/60", "unfulfilled"} "submitted" -> {"bg-blue-50 text-blue-700", "submitted"} "processing" -> {"bg-amber-50 text-amber-700", "processing"} "shipped" -> {"bg-purple-50 text-purple-700", "shipped"} "delivered" -> {"bg-green-50 text-green-700", "delivered"} "failed" -> {"bg-red-50 text-red-700", "failed"} - _ -> {"bg-zinc-100 text-zinc-600", assigns.status || "—"} + _ -> {"bg-base-200 text-base-content/60", assigns.status || "—"} end assigns = assign(assigns, color: color, label: label) @@ -195,6 +744,10 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do """ end + # ========================================================================== + # Helpers + # ========================================================================== + defp format_revenue(amount_pence) when is_integer(amount_pence) do Cart.format_price(amount_pence) end @@ -204,4 +757,31 @@ defmodule SimpleshopThemeWeb.Admin.Dashboard do defp format_date(datetime) do Calendar.strftime(datetime, "%d %b %Y") end + + defp encrypt_api_key(api_key) do + case SimpleshopTheme.Vault.encrypt(api_key) do + {:ok, encrypted} -> encrypted + _ -> nil + end + end + + defp maybe_add_shop_config(params, {:ok, %{shop_id: shop_id}}) do + config = Map.get(params, "config", %{}) |> Map.put("shop_id", to_string(shop_id)) + Map.put(params, "config", config) + end + + defp maybe_add_shop_config(params, _), do: params + + defp maybe_add_name(params, {:ok, %{shop_name: shop_name}}) when is_binary(shop_name) do + Map.put_new(params, "name", shop_name) + end + + defp maybe_add_name(params, _), do: Map.put_new(params, "name", "Printify") + + defp format_printify_error(:no_api_key), do: "Please enter your API token" + defp format_printify_error(:unauthorized), do: "That token doesn't seem to be valid" + defp format_printify_error(:timeout), do: "Couldn't reach Printify — try again" + defp format_printify_error({:http_error, _code}), do: "Something went wrong — try again" + defp format_printify_error(error) when is_binary(error), do: error + defp format_printify_error(_), do: "Connection failed — check your token and try again" end diff --git a/test/simpleshop_theme_web/live/admin/dashboard_test.exs b/test/simpleshop_theme_web/live/admin/dashboard_test.exs index 48590d3..1e00d37 100644 --- a/test/simpleshop_theme_web/live/admin/dashboard_test.exs +++ b/test/simpleshop_theme_web/live/admin/dashboard_test.exs @@ -4,6 +4,7 @@ defmodule SimpleshopThemeWeb.Admin.DashboardTest do import Phoenix.LiveViewTest import SimpleshopTheme.AccountsFixtures import SimpleshopTheme.OrdersFixtures + import SimpleshopTheme.ProductsFixtures setup do user = user_fixture() @@ -18,26 +19,72 @@ defmodule SimpleshopThemeWeb.Admin.DashboardTest do end end - describe "setup checklist" do + describe "setup stepper" do setup %{conn: conn, user: user} do %{conn: log_in_user(conn, user)} end - test "shows setup progress when shop is offline", %{conn: conn} do + test "shows stepper with printify form when nothing connected", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/admin") - assert html =~ "Setup progress" - assert html =~ "Create admin account" + assert html =~ "Setup steps" assert html =~ "Connect to Printify" + assert html =~ "Printify API token" assert html =~ "Connect Stripe" assert html =~ "Go live" end - test "hides setup checklist when shop is live", %{conn: conn} do + test "shows stripe form when printify is done", %{conn: conn} do + conn_fixture = provider_connection_fixture(%{provider_type: "printify"}) + _product = product_fixture(%{provider_connection: conn_fixture}) + + {:ok, view, _html} = live(conn, ~p"/admin") + + # Printify step should be completed + assert has_element?(view, "li:first-child [class*='bg-green-500']") + # Stripe step should be active with form + assert has_element?(view, "label", "Secret key") + end + + test "shows go live button when all services connected", %{conn: conn} do + conn_fixture = provider_connection_fixture(%{provider_type: "printify"}) + _product = product_fixture(%{provider_connection: conn_fixture}) + {:ok, _} = SimpleshopTheme.Settings.put_secret("stripe_api_key", "sk_test_123") + + {:ok, view, _html} = live(conn, ~p"/admin") + + assert has_element?(view, "button", "Go live") + end + + test "go live shows celebration", %{conn: conn} do + conn_fixture = provider_connection_fixture(%{provider_type: "printify"}) + _product = product_fixture(%{provider_connection: conn_fixture}) + {:ok, _} = SimpleshopTheme.Settings.put_secret("stripe_api_key", "sk_test_123") + + {:ok, view, _html} = live(conn, ~p"/admin") + + html = view |> element("button", "Go live") |> render_click() + + assert html =~ "Your shop is live!" + assert html =~ "View your shop" + assert html =~ "Customise theme" + end + + test "hides stepper when shop is live", %{conn: conn} do {:ok, _} = SimpleshopTheme.Settings.set_site_live(true) {:ok, _view, html} = live(conn, ~p"/admin") - refute html =~ "Setup progress" + refute html =~ "Setup steps" + refute html =~ "Printify API token" + end + + test "completed steps show summary and are collapsible", %{conn: conn} do + conn_fixture = provider_connection_fixture(%{provider_type: "printify"}) + _product = product_fixture(%{provider_connection: conn_fixture}) + + {:ok, _view, html} = live(conn, ~p"/admin") + + assert html =~ "products synced" end end
    Order Date Customer
    {order.order_number}{format_date(order.inserted_at)}{order.customer_email || "—"}{format_date(order.inserted_at)}{order.customer_email || "—"} {Cart.format_price(order.total)} <.fulfilment_pill status={order.fulfilment_status} />