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")
adapter_key = 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, %{})
|> 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, socket}
else
{:noreply,
socket
|> assign(:adapter_key, key)
|> assign(:selected_adapter, Adapters.get(key))
|> assign(:field_errors, %{})
|> assign(:test_result, nil)
|> assign(:test_error, nil)}
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)
|> put_flash(:info, "Settings saved — send a test email to check it works")}
{: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
<%!-- 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}
/>
<%!-- Step 4: Send a test email (only after config saved) --%>
<%!-- 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
<% 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