berrypod/lib/simpleshop_theme_web/live/admin/settings.ex

340 lines
12 KiB
Elixir
Raw Normal View History

defmodule SimpleshopThemeWeb.Admin.Settings do
use SimpleshopThemeWeb, :live_view
alias SimpleshopTheme.Settings
alias SimpleshopTheme.Stripe.Setup, as: StripeSetup
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Settings")
|> assign(:site_live, Settings.site_live?())
|> assign_stripe_state()}
end
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(:advanced_open, false)
|> assign(:connecting, false)
end
@impl true
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("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()
|> put_flash(:info, "Webhook signing secret saved")
{:noreply, socket}
end
end
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
def handle_event("toggle_advanced", _params, socket) do
{:noreply, assign(socket, :advanced_open, !socket.assigns.advanced_open)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="max-w-2xl">
<.header>
Settings
<:subtitle>Shop status, payment providers, and API keys</:subtitle>
</.header>
<section class="mt-10">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold">Shop status</h2>
<%= if @site_live do %>
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
<.icon name="hero-check-circle-mini" class="size-3" /> Live
</span>
<% else %>
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
Offline
</span>
<% end %>
</div>
<p class="mt-2 text-sm text-zinc-600">
<%= if @site_live do %>
Your shop is visible to the public.
<% else %>
Your shop is offline. Visitors see a "coming soon" page.
<% end %>
</p>
<div class="mt-4">
<button
phx-click="toggle_site_live"
class={[
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold shadow-xs",
if(@site_live,
do: "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 ring-1 ring-zinc-300 ring-inset",
else: "bg-green-600 text-white hover:bg-green-500"
)
]}
>
<%= if @site_live do %>
<.icon name="hero-eye-slash-mini" class="size-4" /> Take offline
<% else %>
<.icon name="hero-eye-mini" class="size-4" /> Go live
<% end %>
</button>
</div>
</section>
<section class="mt-10">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold">Stripe</h2>
<%= case @stripe_status do %>
<% :connected -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-green-600/20 ring-inset">
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
</span>
<% :connected_localhost -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-amber-600/20 ring-inset">
<.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode
</span>
<% :not_configured -> %>
<span class="inline-flex items-center gap-1 rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-600 ring-1 ring-zinc-500/10 ring-inset">
Not connected
</span>
<% end %>
</div>
<%= if @stripe_status == :not_configured do %>
<.stripe_setup_form connect_form={@connect_form} connecting={@connecting} />
<% else %>
<.stripe_connected_view
stripe_status={@stripe_status}
stripe_api_key_hint={@stripe_api_key_hint}
stripe_webhook_url={@stripe_webhook_url}
stripe_signing_secret_hint={@stripe_signing_secret_hint}
stripe_has_signing_secret={@stripe_has_signing_secret}
secret_form={@secret_form}
advanced_open={@advanced_open}
/>
<% end %>
</section>
</div>
</Layouts.app>
"""
end
defp stripe_setup_form(assigns) do
~H"""
<div class="mt-4">
<p class="text-sm text-zinc-600">
To accept payments, connect your Stripe account by entering your secret key.
You can find it in your
<a
href="https://dashboard.stripe.com/apikeys"
target="_blank"
rel="noopener"
class="text-zinc-900 underline"
>
Stripe dashboard
</a>
under Developers &rarr; API keys.
</p>
<.form for={@connect_form} phx-submit="connect_stripe" class="mt-6">
<.input
field={@connect_form[:api_key]}
type="password"
label="Secret key"
autocomplete="off"
placeholder="sk_test_... or sk_live_..."
/>
<p class="text-xs text-zinc-500 mt-1">
Starts with <code>sk_test_</code> (test mode) or <code>sk_live_</code> (live mode).
This key is encrypted at rest in the database.
</p>
<div class="mt-4">
<.button phx-disable-with="Connecting...">
Connect Stripe
</.button>
</div>
</.form>
</div>
"""
end
defp stripe_connected_view(assigns) do
~H"""
<div class="mt-4 space-y-4">
<dl class="text-sm">
<div class="flex gap-2 py-1">
<dt class="text-zinc-500 w-28 shrink-0">API key</dt>
<dd><code class="text-zinc-700">{@stripe_api_key_hint}</code></dd>
</div>
<div class="flex gap-2 py-1">
<dt class="text-zinc-500 w-28 shrink-0">Webhook URL</dt>
<dd><code class="text-zinc-700 text-xs break-all">{@stripe_webhook_url}</code></dd>
</div>
<div class="flex gap-2 py-1">
<dt class="text-zinc-500 w-28 shrink-0">Webhook secret</dt>
<dd>
<%= if @stripe_has_signing_secret do %>
<code class="text-zinc-700">{@stripe_signing_secret_hint}</code>
<% else %>
<span class="text-amber-600">Not set</span>
<% end %>
</dd>
</div>
</dl>
<%= if @stripe_status == :connected_localhost do %>
<div class="rounded-md bg-amber-50 p-4 ring-1 ring-amber-600/10 ring-inset">
<p class="text-sm text-amber-800">
Stripe can't reach localhost for webhooks. For local testing, run the Stripe CLI:
</p>
<pre class="mt-2 rounded bg-amber-100 p-2 text-xs text-amber-900 overflow-x-auto">stripe listen --forward-to localhost:4000/webhooks/stripe</pre>
<p class="mt-2 text-xs text-amber-700">
The CLI will output a signing secret starting with <code>whsec_</code>. Enter it below.
</p>
</div>
<.form for={@secret_form} phx-submit="save_signing_secret" class="mt-2">
<.input
field={@secret_form[:signing_secret]}
type="password"
label="Webhook signing secret"
autocomplete="off"
placeholder="whsec_..."
/>
<div class="mt-3">
<.button phx-disable-with="Saving...">Save signing secret</.button>
</div>
</.form>
<% else %>
<div class="border-t border-zinc-200 pt-3">
<button
phx-click="toggle_advanced"
class="flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700"
>
<.icon
name={if @advanced_open, do: "hero-chevron-down-mini", else: "hero-chevron-right-mini"}
class="size-4"
/> Advanced
</button>
<%= if @advanced_open do %>
<div class="mt-3">
<p class="text-xs text-zinc-500 mb-3">
Override the webhook signing secret if you need to use a custom endpoint or the Stripe CLI.
</p>
<.form for={@secret_form} phx-submit="save_signing_secret">
<.input
field={@secret_form[:signing_secret]}
type="password"
label="Webhook signing secret"
autocomplete="off"
placeholder="whsec_..."
/>
<div class="mt-3">
<.button phx-disable-with="Saving...">Save signing secret</.button>
</div>
</.form>
</div>
<% end %>
</div>
<% end %>
<div class="border-t border-zinc-200 pt-4">
<button
phx-click="disconnect_stripe"
data-confirm="This will remove your Stripe API key and delete the webhook endpoint. Are you sure?"
class="text-sm text-red-600 hover:text-red-800"
>
Disconnect Stripe
</button>
</div>
</div>
"""
end
end