defmodule BerrypodWeb.Setup.Onboarding do
use BerrypodWeb, :live_view
alias Berrypod.{Accounts, KeyValidation, 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" => "", "password" => "", "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"]
password = params["password"]
shop_name = String.trim(params["shop_name"] || "")
cond do
shop_name == "" ->
{:noreply, put_flash(socket, :error, "Please enter a shop name")}
email == "" ->
{:noreply, put_flash(socket, :error, "Please enter your email address")}
true ->
Settings.put_setting("site_name", shop_name, "string")
case Accounts.register_and_confirm_admin(%{email: email, password: password}) 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} ->
form = to_form(params, as: :account, errors: changeset.errors, action: :validate)
{:noreply,
socket
|> assign(:account_form, form)
|> 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
case KeyValidation.validate_provider_key(api_key, type) do
{:error, message} ->
form =
to_form(%{"api_key" => api_key},
as: :provider,
errors: [api_key: {message, []}],
action: :validate
)
{:noreply, assign(socket, :provider_form, form)}
{:ok, api_key} ->
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} ->
form =
to_form(%{"api_key" => api_key},
as: :provider,
errors: [api_key: {"Please enter your API token", []}],
action: :validate
)
{:noreply,
socket
|> assign(:provider_connecting, false)
|> assign(:provider_form, form)}
{:error, _reason} ->
form =
to_form(%{"api_key" => api_key},
as: :provider,
errors: [api_key: {"Could not connect. Check your API key and try again", []}],
action: :validate
)
{:noreply,
socket
|> assign(:provider_connecting, false)
|> assign(:provider_form, form)}
end
end
end
# ── Events: Stripe ──
def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do
case KeyValidation.validate_stripe_key(api_key) do
{:error, message} ->
form =
to_form(%{"api_key" => api_key},
as: :stripe,
errors: [api_key: {message, []}],
action: :validate
)
{:noreply, assign(socket, :stripe_form, form)}
{:ok, api_key} ->
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} ->
form =
to_form(%{"api_key" => api_key},
as: :stripe,
errors: [api_key: {"Stripe connection failed: #{message}", []}],
action: :validate
)
{:noreply,
socket
|> assign(:stripe_connecting, false)
|> assign(:stripe_form, form)}
end
end
end
# ── Render ──
@impl true
def render(assigns) do
~H"""
Enter the setup secret from your server logs to get started.
<% else %>Connect your accounts to get going.
<% end %>
Find the setup secret in your server logs or set the SETUP_SECRET
environment variable.
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" required phx-mounted={@current_step == 1 && JS.focus()} /> <.input field={@account_form[:email]} type="email" label="Email address" autocomplete="email" required /> <.input field={@account_form[:password]} type="password" label="Password" placeholder="12 characters minimum" autocomplete="new-password" required />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{@summary}
<% else %>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.setup_hint}. <.external_link href={provider_info.setup_url} class="setup-link"> Open {provider_info.name}
<.form for={@form} phx-submit="connect_provider"> <.input field={@form[:api_key]} type="text" label="API token" placeholder="Paste your token here" autocomplete="off" />Enter your Stripe secret key to accept payments. <.external_link href="https://dashboard.stripe.com/apikeys" class="setup-link"> Open Stripe dashboard
<.form for={@form} phx-submit="connect_stripe"> <.input field={@form[:api_key]} type="text" label="Secret key" autocomplete="off" placeholder="sk_test_... or sk_live_..." phx-mounted={@focus && JS.focus()} />