defmodule BerrypodWeb.Admin.EmailSettings do use BerrypodWeb, :live_view alias Berrypod.Mailer alias Berrypod.Mailer.Adapters alias Berrypod.Settings @impl true def mount(_params, _session, socket) do env_locked = Mailer.env_var_configured?() {current_adapter, _current_values} = Mailer.current_config() saved_adapter = Settings.get_setting("email_adapter") # Controller fallback passes errors via flash after failed validation flash_adapter = Phoenix.Flash.get(socket.assigns.flash, :error_adapter) flash_errors = Phoenix.Flash.get(socket.assigns.flash, :field_errors) || %{} adapter_key = flash_adapter || current_adapter || saved_adapter grouped = Adapters.grouped() all_adapters = Adapters.all() all_values = load_all_adapter_values() {:ok, socket |> assign(:page_title, "Email settings") |> assign(:env_locked, env_locked) |> assign(:adapter_key, adapter_key) |> assign(:all_values, all_values) |> assign(:all_email_adapters, grouped[:all_email] || []) |> assign(:advanced_adapters, grouped[:advanced] || []) |> assign(:all_adapters, all_adapters) |> assign(:email_configured, Mailer.email_configured?()) |> assign(:selected_adapter, adapter_key && Adapters.get(adapter_key)) |> assign(:sending_test, false) |> assign(:test_result, if(Mailer.email_verified?(), do: :ok)) |> assign(:test_error, nil) |> assign(:test_retryable, false) |> assign(:from_checklist, false) |> assign(:field_errors, flash_errors) |> assign(:save_status, :idle) |> assign(:form, to_form(%{}, as: :email))} end @impl true def handle_params(params, _uri, socket) do {:noreply, assign(socket, :from_checklist, params["from"] == "checklist")} end defp load_all_adapter_values do for adapter <- Adapters.all(), into: %{} do values = for field <- adapter.fields, into: %{} do settings_key = Adapters.settings_key(adapter.key, field.key) value = case field.type do :secret -> Settings.secret_hint(settings_key) _ -> Settings.get_setting(settings_key) end {field.key, value} end {adapter.key, values} end end @impl true def handle_event("form_change", %{"email" => %{"adapter" => key}}, socket) do if key == socket.assigns.adapter_key do {:noreply, assign(socket, :save_status, :idle)} else {:noreply, socket |> assign(:adapter_key, key) |> assign(:selected_adapter, Adapters.get(key)) |> assign(:field_errors, %{}) |> assign(:test_result, nil) |> assign(:test_error, nil) |> assign(:save_status, :idle)} end end def handle_event("save", %{"email" => params}, socket) do if socket.assigns.env_locked do {:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")} else adapter_key = params["adapter"] # Fields are namespaced: email[brevo][api_key] → params["brevo"]["api_key"] adapter_params = params[adapter_key] || %{} case Mailer.save_config( adapter_key, adapter_params, socket.assigns.current_scope.user.email ) do {:ok, _adapter_info} -> {:noreply, socket |> assign(:adapter_key, adapter_key) |> assign(:selected_adapter, Adapters.get(adapter_key)) |> assign(:all_values, load_all_adapter_values()) |> assign(:email_configured, Mailer.email_configured?()) |> assign(:field_errors, %{}) |> assign(:test_result, nil) |> assign(:test_error, nil) |> assign(:save_status, :saved)} {:error, field_errors} when is_map(field_errors) -> {:noreply, assign(socket, :field_errors, field_errors)} end end end def handle_event("send_test", _params, socket) do send(self(), :do_send_test) {:noreply, assign(socket, :sending_test, true)} end @impl true def handle_info(:do_send_test, socket) do user = socket.assigns.current_scope.user case Mailer.send_test_email(user.email, Mailer.from_address()) do {:ok, _} -> Mailer.mark_email_verified() {:noreply, socket |> assign(:sending_test, false) |> assign(:test_result, :ok) |> assign(:test_error, nil)} {:error, reason} -> {:noreply, socket |> assign(:sending_test, false) |> assign(:test_result, :error) |> assign(:test_error, Mailer.friendly_error(reason)) |> assign(:test_retryable, Mailer.retryable_error?(reason))} end end # Swoosh test adapter sends {:email, ...} messages — ignore them def handle_info({:email, _}, socket), do: {:noreply, socket} @impl true def render(assigns) do ~H"""
<.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" /> You're setting up email for your shop. <.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link"> ← Back to checklist
<.header> Email settings <:subtitle> Your shop needs an email provider to send order confirmations, shipping updates, and newsletters to your customers. <%= if @env_locked do %>
<.icon name="hero-lock-closed" class="size-5" />

Controlled by environment variables

Email is configured via SMTP_HOST and related env vars. Remove them to configure email from this page instead.

<% end %>
<.form for={@form} action={~p"/admin/settings/email"} method="post" phx-change="form_change" phx-submit="save" > <%!-- Step 1: Choose a provider --%>

1 Choose a provider

Email provider
<.provider_card :for={adapter <- @all_email_adapters} adapter={adapter} selected={@adapter_key} disabled={@env_locked} />
Already have your own email server?
<.provider_card :for={adapter <- @advanced_adapters} adapter={adapter} selected={@adapter_key} disabled={@env_locked} />
<%!-- Steps 2 & 3: config for each adapter (hidden keeps HTML correct without CSS) --%> <.adapter_config :for={adapter <- @all_adapters} adapter={adapter} selected={@adapter_key == adapter.key} values={@all_values[adapter.key] || %{}} field_errors={if(@adapter_key == adapter.key, do: @field_errors, else: %{})} env_locked={@env_locked} save_status={if(@adapter_key == adapter.key, do: @save_status, else: :idle)} />
<%!-- Step 4: Send a test email (only after config saved) --%>

<%= cond do %> <% @test_result == :ok -> %> <.icon name="hero-check-mini" class="size-4" /> <% @test_result == :error -> %> <.icon name="hero-x-mark-mini" class="size-4" /> <% true -> %> {if @selected_adapter && @selected_adapter.url, do: "4", else: "3"} <% end %> <%= cond do %> <% @test_result == :ok -> %> Email is working <% @test_result == :error -> %> Test failed <% true -> %> Send a test email <% end %>

<%= if @test_result == :ok do %>

Test email sent to {@current_scope.user.email}. Check your inbox to confirm it arrived.

<.link :if={@from_checklist} navigate={~p"/admin"} class="admin-btn admin-btn-primary admin-btn-sm" > Continue setup → <.button type="button" phx-click="send_test"> Send again
<% else %> <%= if @test_result == :error do %>

{@test_error}

<%= if @test_retryable do %> <.button type="button" phx-click="send_test" disabled={@sending_test} class="admin-btn admin-btn-outline admin-btn-error" > <.icon name="hero-paper-airplane" class="size-4" /> {if @sending_test, do: "Sending...", else: "Try again"} <% else %>

Fix your settings above and reconnect, then try the test again.

<% end %>
<% else %>

Send a test to {@current_scope.user.email} to check everything works.

<.form for={%{}} action={~p"/admin/settings/email/test"} phx-submit="send_test" method="post" class="admin-row admin-row-sm" > <.button disabled={@sending_test} phx-disable-with="Sending..."> <.icon name="hero-paper-airplane" class="size-4" /> Send test email <% end %> <% end %>
""" end # ── Local components ── attr :adapter, :map, required: true attr :selected, :boolean, default: false attr :values, :map, required: true attr :field_errors, :map, required: true attr :env_locked, :boolean, required: true attr :save_status, :atom, default: :idle defp adapter_config(assigns) do ~H"""
Configure {@adapter.name} <%!-- Create an account (providers with sign-up URLs) --%>

2 Create a free account

<.external_link href={@adapter.url} class="admin-link"> Sign up at {@adapter.name} ↗ if you don't already have an account. It's free.

<%!-- Paste your key / server details --%>

{if @adapter.url, do: "3", else: "2"} {adapter_fields_title(@adapter)}

{adapter_fields_instruction(@adapter)}

<%= for field <- @adapter.fields do %> <.adapter_field_input adapter_key={@adapter.key} field_def={field} value={@values[field.key]} disabled={@env_locked} error={@field_errors[field.key]} /> <% end %> <%= unless @env_locked do %>
<.button phx-disable-with="Saving..."> Save settings <.inline_feedback status={@save_status} message="Now send a test email" />
<% end %>
""" end defp adapter_fields_title(%{key: "smtp"}), do: "Enter your server details" defp adapter_fields_title(%{key: "postal"}), do: "Enter your server details" defp adapter_fields_title(%{key: "mailjet"}), do: "Paste your API keys" defp adapter_fields_title(_adapter), do: "Paste your API key" defp adapter_fields_instruction(%{key: "smtp"}), do: "Enter your SMTP server connection details below." defp adapter_fields_instruction(%{key: "postal"}), do: "Enter your Postal server URL and API key below." defp adapter_fields_instruction(%{key: "mailjet"}), do: "Find your API key and secret key under API Key Management in your Mailjet account." defp adapter_fields_instruction(%{key: "mailgun"}), do: "Find your API key in your Mailgun dashboard, and enter your sending domain." defp adapter_fields_instruction(adapter), do: "Find your API key in your #{adapter.name} account settings and paste it here." attr :adapter, :map, required: true attr :selected, :string, default: nil attr :disabled, :boolean, default: false defp provider_card(assigns) do ~H""" """ end # ── Field renderers ── # Fields are namespaced per adapter: email[brevo][api_key], email[smtp][relay], etc. attr :adapter_key, :string, required: true attr :field_def, :map, required: true attr :value, :any, default: nil attr :disabled, :boolean, default: false attr :error, :string, default: nil defp adapter_field_input(%{field_def: %{type: :secret}} = assigns) do assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: [])) ~H""" <.input name={"email[#{@adapter_key}][#{@field_def.key}]"} value="" type="text" label={@field_def.label} autocomplete="off" placeholder={@value || ""} disabled={@disabled} errors={@errors} /> """ end defp adapter_field_input(%{field_def: %{type: :integer}} = assigns) do assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: [])) ~H""" <.input name={"email[#{@adapter_key}][#{@field_def.key}]"} value={@value || @field_def.default || ""} type="number" label={@field_def.label} disabled={@disabled} errors={@errors} /> """ end defp adapter_field_input(assigns) do assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: [])) ~H""" <.input name={"email[#{@adapter_key}][#{@field_def.key}]"} value={@value || @field_def.default || ""} type="text" label={@field_def.label} disabled={@disabled} errors={@errors} /> """ end end