defmodule BerrypodWeb.Setup.Onboarding do use BerrypodWeb, :live_view alias Berrypod.{Accounts, Products, Settings, Setup} alias Berrypod.Products.ProviderConnection 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, mount_check_inbox(socket)} setup.admin_created -> {:ok, mount_configure(socket, setup)} true -> {:ok, mount_email_form(socket)} 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_email_form(socket) do socket |> assign(:page_title, "Set up your shop") |> assign(:phase, :email_form) |> assign(:account_form, to_form(%{"email" => ""}, as: :account)) end defp mount_check_inbox(socket) do socket |> assign(:page_title, "Set up your shop") |> assign(:phase, :check_inbox) |> assign(:admin_email, Accounts.admin_email()) |> assign(:local_mail?, local_mail_adapter?()) end defp mount_configure(socket, setup) do provider_conn = Products.get_first_provider_connection() socket |> assign(:page_title, "Set up your shop") |> assign(:phase, :configure) |> assign(:setup, setup) # Provider |> assign(:providers, Provider.all()) |> assign(:selected_provider, nil) |> assign(:provider_form, to_form(%{"api_key" => ""}, as: :provider)) |> assign(:provider_testing, false) |> assign(:provider_test_result, nil) |> assign(:provider_connecting, false) |> assign(:provider_conn, provider_conn) |> assign(:pending_provider_key, nil) # Stripe |> assign(:stripe_form, to_form(%{"api_key" => ""}, as: :stripe)) |> assign(:stripe_connecting, false) end # ── Events: Account ── @impl true def handle_event("create_account", %{"account" => %{"email" => email}}, socket) do if email == "" do {:noreply, put_flash(socket, :error, "Please enter your email address")} else case Accounts.register_user(%{email: email}) do {:ok, user} -> {:ok, _} = Accounts.deliver_login_instructions( user, &url(~p"/users/log-in/#{&1}") ) {:noreply, socket |> assign(:phase, :check_inbox) |> assign(:admin_email, user.email) |> assign(:local_mail?, local_mail_adapter?())} {:error, changeset} -> {:noreply, socket |> assign(:account_form, to_form(changeset, as: :account)) |> put_flash(:error, "Could not create account")} end end end def handle_event("start_over", _params, socket) do case Accounts.get_unconfirmed_admin() do %Accounts.User{} = user -> case Accounts.delete_unconfirmed_user(user) do {:ok, _} -> {:noreply, socket |> assign(:phase, :email_form) |> assign(:account_form, to_form(%{"email" => ""}, as: :account)) |> clear_flash()} {:error, :already_confirmed} -> {:noreply, push_navigate(socket, to: ~p"/setup")} end nil -> {:noreply, push_navigate(socket, to: ~p"/setup")} end end # ── Events: Provider ── def handle_event("select_provider", %{"type" => type}, socket) do {:noreply, socket |> assign(:selected_provider, type) |> assign(:provider_form, to_form(%{"api_key" => ""}, as: :provider)) |> assign(:provider_test_result, nil) |> assign(:pending_provider_key, nil)} end def handle_event("validate_provider", %{"provider" => params}, socket) do {:noreply, assign(socket, pending_provider_key: params["api_key"])} end def handle_event("test_provider", _params, socket) do type = socket.assigns.selected_provider api_key = socket.assigns.pending_provider_key if api_key in [nil, ""] do {:noreply, assign(socket, provider_test_result: {:error, :no_api_key})} else socket = assign(socket, provider_testing: true, provider_test_result: nil) temp_conn = %ProviderConnection{ provider_type: type, api_key_encrypted: encrypt_api_key(api_key) } result = Berrypod.Providers.test_connection(temp_conn) {:noreply, assign(socket, provider_testing: false, provider_test_result: result)} end 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) params = %{"api_key" => api_key, "provider_type" => type} |> maybe_add_shop_config(socket.assigns.provider_test_result) |> maybe_add_name(socket.assigns.provider_test_result, type) case Products.create_provider_connection(params) do {:ok, connection} -> Products.enqueue_sync(connection) setup = %{ socket.assigns.setup | provider_connected: true, provider_type: type } {:noreply, socket |> assign(:provider_connecting, false) |> assign(:provider_conn, connection) |> assign(:setup, setup) |> put_flash(:info, "Connected! Product sync started in the background.")} {:error, _changeset} -> {:noreply, socket |> assign(:provider_connecting, false) |> put_flash(:error, "Failed to save connection")} 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 = %{socket.assigns.setup | stripe_connected: true} setup = if setup.admin_created and setup.provider_connected do %{setup | setup_complete: true} else setup end {:noreply, socket |> assign(:stripe_connecting, false) |> assign(:setup, setup) |> put_flash(:info, "Stripe connected")} {: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

Enter your email to create the admin account.

Almost there — check your inbox.

Connect your accounts to get going.

<%!-- Phase 1: email form --%>
<.section_card title="Create admin account" number={1} done={false}>

We'll send a login link to get you started.

<.form for={@account_form} phx-submit="create_account"> <.input field={@account_form[:email]} type="email" label="Email address" autocomplete="email" required phx-mounted={JS.focus()} />
<.button phx-disable-with="Creating account...">Create account
<%!-- Phase 2: check inbox --%>
<.icon name="hero-envelope" class="size-10 setup-inbox-icon" />

Check your inbox

We sent a login link to {@admin_email}. Click it to continue setting up your shop.

<.icon name="hero-information-circle" class="size-5 shrink-0" />

Using local mail adapter. See sent emails at /dev/mailbox.

Wrong email?

<%!-- Phase 3: configure (logged in) --%>
<.section_card title="Create admin account" number={1} done={true} summary={account_summary(assigns)} > <.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} testing={@provider_testing} test_result={@provider_test_result} 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} />
<%!-- 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 # ── 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 :testing, :boolean, required: true attr :test_result, :any, default: nil attr :connecting, :boolean, required: true defp provider_section(assigns) do ~H"""

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

<%!-- 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-change="validate_provider" 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 or @testing}> {if @connecting, do: "Connecting...", else: "Connect"}
<.provider_test_feedback :if={@test_result} result={@test_result} />
""" end attr :result, :any, required: true defp provider_test_feedback(assigns) do ~H"""
<%= case @result do %> <% {:ok, info} -> %> <.icon name="hero-check-circle" class="size-4" /> Connected{if info[:shop_name], do: " to #{info.shop_name}", else: ""} <% {:error, :no_api_key} -> %> <.icon name="hero-x-circle" class="size-4" /> Please enter your API token <% {:error, reason} -> %> <.icon name="hero-x-circle" class="size-4" /> {format_error(reason)} <% end %>
""" end # ── Stripe section ── attr :form, :any, required: true attr :connecting, :boolean, required: true 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_..." />

Starts with sk_test_ or sk_live_. Encrypted at rest.

<.button phx-disable-with="Connecting..."> {if @connecting, do: "Connecting...", else: "Connect Stripe"}
""" end # ── Helpers ── defp account_summary(%{current_scope: %{user: user}}) when not is_nil(user) do user.email 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 encrypt_api_key(api_key) do case Berrypod.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: name}}, _type) when is_binary(name) do Map.put_new(params, "name", name) end defp maybe_add_name(params, _, type) do case Provider.get(type) do nil -> Map.put_new(params, "name", type) info -> Map.put_new(params, "name", info.name) end end defp format_error(:unauthorized), do: "That token doesn't seem to be valid" defp format_error(:timeout), do: "Couldn't reach the provider — try again" defp format_error(:provider_not_implemented), do: "This provider isn't supported yet" defp format_error({:http_error, _code}), do: "Something went wrong — try again" defp format_error(error) when is_binary(error), do: error defp format_error(_), do: "Connection failed — check your token and try again" defp local_mail_adapter? do Application.get_env(:berrypod, Berrypod.Mailer)[:adapter] == Swoosh.Adapters.Local end end