defmodule BerrypodWeb.Setup.Onboarding do use BerrypodWeb, :live_view alias Berrypod.{Accounts, Products, Settings, Setup} alias Berrypod.Providers.Provider alias Berrypod.Stripe.Setup, as: StripeSetup # ── Mount ── @impl true def mount(_params, _session, socket) do setup = Setup.setup_status() cond do setup.site_live -> {:ok, push_navigate(socket, to: ~p"/")} setup.setup_complete -> {:ok, push_navigate(socket, to: ~p"/admin")} setup.admin_created and is_nil(get_user(socket)) -> {:ok, push_navigate(socket, to: ~p"/users/log-in")} true -> {:ok, mount_setup(socket, setup)} end end defp get_user(socket) do case socket.assigns do %{current_scope: %{user: user}} when not is_nil(user) -> user _ -> nil end end defp mount_setup(socket, setup) do logged_in? = get_user(socket) != nil provider_conn = Products.get_first_provider_connection() current_step = cond do not logged_in? -> 1 not setup.provider_connected -> 2 true -> 3 end socket |> assign(:page_title, "Set up your shop") |> assign(:setup, setup) |> assign(:logged_in?, logged_in?) |> assign(:current_step, current_step) # Secret gate |> assign(:require_secret?, Setup.require_setup_secret?()) |> assign(:secret_verified, false) |> assign(:secret_form, to_form(%{"secret" => ""}, as: :secret)) # Account (card 1) |> assign(:account_form, to_form(%{"email" => "", "shop_name" => ""}, as: :account)) # Provider (card 2) |> assign(:providers, Provider.all()) |> assign(:selected_provider, nil) |> assign(:provider_form, to_form(%{"api_key" => ""}, as: :provider)) |> assign(:provider_connecting, false) |> assign(:provider_conn, provider_conn) # Stripe (card 3) |> assign(:stripe_form, to_form(%{"api_key" => ""}, as: :stripe)) |> assign(:stripe_connecting, false) end # ── Events: Secret gate ── @impl true def handle_event("verify_secret", %{"secret" => %{"secret" => secret}}, socket) do if Plug.Crypto.secure_compare(secret, Setup.setup_secret()) do {:noreply, assign(socket, secret_verified: true)} else {:noreply, put_flash(socket, :error, "Wrong setup secret")} end end # ── Events: Account ── def handle_event("create_account", %{"account" => params}, socket) do email = params["email"] shop_name = String.trim(params["shop_name"] || "") if email == "" do {:noreply, put_flash(socket, :error, "Please enter your email address")} else if shop_name != "" do Settings.put_setting("site_name", shop_name, "string") end case Accounts.register_and_confirm_admin(%{email: email}) do {:ok, user} -> token = Accounts.generate_login_token(user) {:noreply, redirect(socket, to: ~p"/setup/login/#{token}")} {:error, :admin_already_exists} -> {:noreply, socket |> put_flash(:error, "An admin account already exists") |> push_navigate(to: ~p"/setup")} {:error, changeset} -> {:noreply, socket |> assign(:account_form, to_form(changeset, as: :account)) |> put_flash(:error, "Could not create account")} end end end # ── Events: Provider ── def handle_event("select_provider", %{"provider_select" => %{"type" => type}}, socket) do {:noreply, socket |> assign(:selected_provider, type) |> assign(:provider_form, to_form(%{"api_key" => ""}, as: :provider))} end def handle_event("connect_provider", %{"provider" => %{"api_key" => api_key}}, socket) do type = socket.assigns.selected_provider if api_key == "" do {:noreply, put_flash(socket, :error, "Please enter your API token")} else socket = assign(socket, provider_connecting: true) case Products.connect_provider(api_key, type) do {:ok, connection} -> setup = Setup.setup_status() if setup.setup_complete do {:noreply, socket |> put_flash(:info, "You're in! Here's your launch checklist.") |> push_navigate(to: ~p"/admin")} else {:noreply, socket |> assign(:provider_connecting, false) |> assign(:provider_conn, connection) |> assign(:setup, setup) |> put_flash(:info, "Connected! Product sync started in the background.")} end {:error, :no_api_key} -> {:noreply, socket |> assign(:provider_connecting, false) |> put_flash(:error, "Please enter your API token")} {:error, _reason} -> {:noreply, socket |> assign(:provider_connecting, false) |> put_flash(:error, "Could not connect — check your API key and try again")} end 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} -> setup = Setup.setup_status() if setup.setup_complete do {:noreply, socket |> put_flash(:info, "You're in! Here's your launch checklist.") |> push_navigate(to: ~p"/admin")} else {:noreply, socket |> assign(:stripe_connecting, false) |> assign(:setup, setup) |> put_flash(:info, "Stripe connected")} end {:error, message} -> {:noreply, socket |> assign(:stripe_connecting, false) |> put_flash(:error, "Stripe connection failed: #{message}")} end end end # ── Render ── @impl true def render(assigns) do ~H"""

Set up your shop

<%= if @require_secret? and not @secret_verified do %>

Enter the setup secret from your server logs to get started.

<% else %>

Connect your accounts to get going.

<% end %>
<%!-- Secret gate (prod only, before admin exists) --%> <%= if @require_secret? and not @secret_verified do %>

Find the setup secret in your server logs or set the SETUP_SECRET environment variable.

<.form for={@secret_form} phx-submit="verify_secret"> <.input field={@secret_form[:secret]} type="password" label="Setup secret" autocomplete="off" required phx-mounted={JS.focus()} />
<.button phx-disable-with="Checking...">Continue
<% else %> <%!-- All three setup cards --%>
<.section_card title="Set up your account" number={1} done={@logged_in?} summary={account_summary(assigns)} >

Name your shop and create the admin account.

<.form for={@account_form} phx-submit="create_account"> <.input field={@account_form[:shop_name]} type="text" label="Shop name" placeholder="e.g. Acme Prints" autocomplete="off" phx-mounted={@current_step == 1 && JS.focus()} /> <.input field={@account_form[:email]} type="email" label="Email address" autocomplete="email" required />
<.button phx-disable-with="Creating account...">Create account
<.section_card title="Connect a print provider" number={2} done={@setup.provider_connected} summary={provider_summary(assigns)} > <.provider_section providers={@providers} selected={@selected_provider} form={@provider_form} connecting={@provider_connecting} /> <.section_card title="Connect payments" number={3} done={@setup.stripe_connected} summary={stripe_summary(assigns)} > <.stripe_section form={@stripe_form} connecting={@stripe_connecting} focus={@current_step == 3} />
<%!-- All done --%>
<.icon name="hero-check-badge" class="setup-complete-icon" />

You're all set

Head to the dashboard to sync products, customise your theme, and go live.

<.link navigate={~p"/admin"} class="admin-btn admin-btn-primary"> Go to dashboard
<% end %>
""" end # ── Section card component ── attr :title, :string, required: true attr :number, :integer, required: true attr :done, :boolean, required: true attr :summary, :string, default: nil slot :inner_block, required: true defp section_card(assigns) do ~H"""
<%= if @done do %> <.icon name="hero-check-mini" class="size-4" /> <% else %> {@number} <% end %>

{@title}

<%= if @done and @summary do %>

{@summary}

<% else %>
{render_slot(@inner_block)}
<% end %>
""" end # ── Provider section ── attr :providers, :list, required: true attr :selected, :string, default: nil attr :form, :any, required: true attr :connecting, :boolean, required: true defp provider_section(assigns) do assigns = assign(assigns, :provider_options, provider_card_options(assigns.providers)) ~H"""

Choose a print-on-demand provider and connect your API key.

<.form for={%{}} as={:provider_select} phx-change="select_provider"> <.card_radio_group name="provider_select[type]" value={@selected} legend="Print provider" options={@provider_options} /> <%!-- API key form for selected provider --%>
<% provider_info = Enum.find(@providers, &(&1.type == @selected)) %>

{provider_info.setup_hint}. Open {provider_info.name} →

<.form for={@form} phx-submit="connect_provider"> <.input field={@form[:api_key]} type="password" label="API token" placeholder="Paste your token here" autocomplete="off" />
<.button type="submit" disabled={@connecting}> {if @connecting, do: "Connecting...", else: "Connect"}
""" end # ── Stripe section ── attr :form, :any, required: true attr :connecting, :boolean, required: true attr :focus, :boolean, default: false defp stripe_section(assigns) do ~H"""

Enter your Stripe secret key to accept payments. Open Stripe dashboard →

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

Starts with sk_test_ or sk_live_. Encrypted at rest.

<.button phx-disable-with="Connecting..."> {if @connecting, do: "Connecting...", else: "Connect"}
""" end # ── Helpers ── defp account_summary(%{current_scope: %{user: user}}) when not is_nil(user) do site_name = Settings.site_name() if site_name != "Store Name" do "#{site_name} · #{user.email}" else user.email end end defp account_summary(_), do: "Account created" defp provider_summary(%{setup: %{provider_type: type}}) when is_binary(type) do case Provider.get(type) do nil -> "Connected" info -> "Connected to #{info.name}" end end defp provider_summary(_), do: nil defp stripe_summary(%{setup: %{stripe_connected: true}}) do case Settings.secret_hint("stripe_api_key") do nil -> "Connected" hint -> "Connected · #{hint}" end end defp stripe_summary(_), do: nil defp provider_card_options(providers) do Enum.map(providers, fn provider -> option = %{ value: provider.type, name: provider.name, description: provider.tagline } if provider.status == :coming_soon do Map.merge(option, %{badge: "Coming soon", disabled: true}) else option end end) end end