defmodule BerrypodWeb.Admin.Settings do use BerrypodWeb, :live_view alias Berrypod.Products alias Berrypod.Settings alias Berrypod.Stripe.Setup, as: StripeSetup @impl true def mount(_params, _session, socket) do user = socket.assigns.current_scope.user {:ok, socket |> assign(:page_title, "Settings") |> assign(:site_live, Settings.site_live?()) |> assign(:cart_recovery_enabled, Settings.abandoned_cart_recovery_enabled?()) |> assign(:from_address, Settings.get_setting("email_from_address") || user.email) |> assign(:from_address_status, :idle) |> assign(:signing_secret_status, :idle) |> assign_stripe_state() |> assign_products_state() |> assign_url_prefixes()} end defp assign_url_prefixes(socket) do socket |> assign(:products_prefix, Settings.get_url_prefix(:products)) |> assign(:collections_prefix, Settings.get_url_prefix(:collections)) |> assign(:prefix_status, :idle) end # -- Stripe assigns -- defp assign_stripe_state(socket) do has_key = Settings.has_secret?("stripe_api_key") has_signing = Settings.has_secret?("stripe_signing_secret") status = cond do !has_key -> :not_configured has_key && StripeSetup.localhost?() -> :connected_localhost true -> :connected end socket |> assign(:stripe_status, status) |> assign(:stripe_api_key_hint, Settings.secret_hint("stripe_api_key")) |> assign(:stripe_signing_secret_hint, Settings.secret_hint("stripe_signing_secret")) |> assign(:stripe_webhook_url, StripeSetup.webhook_url()) |> assign(:stripe_has_signing_secret, has_signing) |> assign(:connect_form, to_form(%{"api_key" => ""}, as: :stripe)) |> assign(:secret_form, to_form(%{"signing_secret" => ""}, as: :webhook)) |> assign(:stripe_advanced_open, false) |> assign(:connecting, false) end # -- Products assigns -- defp assign_products_state(socket) do connections = Products.list_provider_connections() connection_info = case connections do [conn | _] -> product_count = Products.count_products_for_connection(conn.id) %{connection: conn, product_count: product_count} [] -> nil end assign(socket, :provider, connection_info) end # -- Events: shop status -- @impl true def handle_event("toggle_site_live", _params, socket) do new_value = !socket.assigns.site_live {:ok, _} = Settings.set_site_live(new_value) message = if new_value, do: "Shop is now live", else: "Shop taken offline" {:noreply, socket |> assign(:site_live, new_value) |> put_flash(:info, message)} end # -- Events: cart recovery -- def handle_event("toggle_cart_recovery", _params, socket) do new_value = !socket.assigns.cart_recovery_enabled {:ok, _} = Settings.set_abandoned_cart_recovery(new_value) message = if new_value, do: "Cart recovery emails enabled", else: "Cart recovery emails disabled" {:noreply, socket |> assign(:cart_recovery_enabled, new_value) |> put_flash(:info, message)} end # -- Events: from address -- def handle_event("change_from_address", _params, socket) do {:noreply, assign(socket, :from_address_status, :idle)} end def handle_event("save_from_address", %{"from_address" => address}, socket) do address = String.trim(address) if address != "" do Settings.put_setting("email_from_address", address) {:noreply, socket |> assign(:from_address, address) |> assign(:from_address_status, :saved)} else {:noreply, put_flash(socket, :error, "From address can't be blank")} end end # -- Events: URL prefixes -- def handle_event("change_prefix", _params, socket) do {:noreply, assign(socket, :prefix_status, :idle)} end def handle_event("save_prefixes", %{"prefixes" => params}, socket) do products_prefix = params["products"] || "" collections_prefix = params["collections"] || "" errors = [] # Update products prefix if changed errors = if products_prefix != socket.assigns.products_prefix do case Settings.update_url_prefix(:products, products_prefix) do {:ok, _} -> errors {:error, reason} -> [{:products, reason} | errors] end else errors end # Update collections prefix if changed errors = if collections_prefix != socket.assigns.collections_prefix do case Settings.update_url_prefix(:collections, collections_prefix) do {:ok, _} -> errors {:error, reason} -> [{:collections, reason} | errors] end else errors end if errors == [] do {:noreply, socket |> assign_url_prefixes() |> assign(:prefix_status, :saved)} else error_message = format_prefix_errors(errors) {:noreply, put_flash(socket, :error, error_message)} 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, :connecting, true) case StripeSetup.connect(api_key) do {:ok, :webhook_created} -> socket = socket |> assign_stripe_state() |> put_flash(:info, "Stripe connected and webhook endpoint created") {:noreply, socket} {:ok, :localhost} -> socket = socket |> assign_stripe_state() |> put_flash( :info, "API key saved. Enter a webhook signing secret below for local testing." ) {:noreply, socket} {:error, message} -> socket = socket |> assign(:connecting, false) |> put_flash(:error, "Stripe connection failed: #{message}") {:noreply, socket} end end end def handle_event("disconnect_stripe", _params, socket) do StripeSetup.disconnect() socket = socket |> assign_stripe_state() |> put_flash(:info, "Stripe disconnected") {:noreply, socket} end def handle_event("change_signing_secret", _params, socket) do {:noreply, assign(socket, :signing_secret_status, :idle)} end def handle_event("save_signing_secret", %{"webhook" => %{"signing_secret" => secret}}, socket) do if secret == "" do {:noreply, put_flash(socket, :error, "Please enter a signing secret")} else StripeSetup.save_signing_secret(secret) socket = socket |> assign_stripe_state() |> assign(:signing_secret_status, :saved) {:noreply, socket} end end def handle_event("toggle_stripe_advanced", _params, socket) do {:noreply, assign(socket, :stripe_advanced_open, !socket.assigns.stripe_advanced_open)} end # -- Events: products -- def handle_event("sync", %{"id" => id}, socket) do connection = Products.get_provider_connection!(id) case Products.enqueue_sync(connection) do {:ok, _job} -> {:noreply, socket |> assign_products_state() |> put_flash(:info, "Sync started for #{connection.name}")} {:error, _reason} -> {:noreply, put_flash(socket, :error, "Failed to start sync")} end end def handle_event("delete_connection", %{"id" => id}, socket) do connection = Products.get_provider_connection!(id) {:ok, _} = Products.delete_provider_connection(connection) {:noreply, socket |> assign_products_state() |> put_flash(:info, "Provider connection deleted")} end # -- Render -- @impl true def render(assigns) do ~H"""
<%= if @site_live do %> Your shop is visible to the public. <% else %> Your shop is offline. Visitors see a "coming soon" page. <% end %>
Connect a print-on-demand provider to import products into your shop.
When on, customers who entered their email at Stripe checkout but didn't complete payment receive a single plain-text recovery email one hour later. No tracking pixels. One email, never more.
<%= if @cart_recovery_enabled do %>Make sure your privacy policy mentions that a single recovery email may be sent, and that customers can unsubscribe at any time.
<% end %>The sender address on all emails from your shop.
Customise the URL structure for products and collections. Old URLs will automatically redirect to the new ones.
To accept payments, connect your Stripe account by entering your secret key. You can find it in your <.external_link href="https://dashboard.stripe.com/apikeys" class="admin-link"> Stripe dashboard under Developers → API keys.
<.form for={@connect_form} phx-submit="connect_stripe" class="admin-card-spaced"> <.input field={@connect_form[:api_key]} type="password" label="Secret key" autocomplete="off" placeholder="sk_test_... or sk_live_..." />
Starts with sk_test_ (test mode) or sk_live_ (live mode).
This key is encrypted at rest in the database.
{@stripe_api_key_hint}{@stripe_webhook_url}
{@stripe_signing_secret_hint}
<% else %>
Not set
<% end %>
Stripe can't reach localhost for webhooks. For local testing, run the Stripe CLI:
stripe listen --forward-to localhost:4000/webhooks/stripe
The CLI will output a signing secret starting with whsec_. Enter it below.
Override the webhook signing secret if you need to use a custom endpoint or the Stripe CLI.
<.form for={@secret_form} action={~p"/admin/settings/stripe/signing-secret"} method="post" phx-change="change_signing_secret" phx-submit="save_signing_secret" > <.input field={@secret_form[:signing_secret]} type="password" label="Webhook signing secret" autocomplete="off" placeholder="whsec_..." />