2026-02-21 19:29:34 +00:00
|
|
|
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
|
2026-03-04 17:12:10 +00:00
|
|
|
grouped = Adapters.grouped()
|
2026-02-21 19:29:34 +00:00
|
|
|
|
|
|
|
|
{:ok,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:page_title, "Email settings")
|
|
|
|
|
|> assign(:env_locked, env_locked)
|
|
|
|
|
|> assign(:adapter_key, adapter_key)
|
|
|
|
|
|> assign(:current_values, current_values)
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
|> assign(:all_email_adapters, grouped[:all_email] || [])
|
|
|
|
|
|> assign(:transactional_adapters, grouped[:transactional] || [])
|
2026-03-04 17:12:10 +00:00
|
|
|
|> assign(:advanced_adapters, grouped[:advanced] || [])
|
2026-02-21 19:29:34 +00:00
|
|
|
|> assign(:email_configured, Mailer.email_configured?())
|
2026-03-04 17:12:10 +00:00
|
|
|
|> assign(:selected_adapter, adapter_key && Adapters.get(adapter_key))
|
2026-02-21 19:29:34 +00:00
|
|
|
|> assign(:sending_test, false)
|
2026-03-04 17:12:10 +00:00
|
|
|
|> assign(:test_result, if(Mailer.email_verified?(), do: :ok))
|
|
|
|
|
|> assign(:test_error, nil)
|
|
|
|
|
|> assign(:test_retryable, false)
|
2026-03-03 17:41:08 +00:00
|
|
|
|> assign(:from_checklist, false)
|
2026-03-04 12:17:56 +00:00
|
|
|
|> assign(:field_errors, %{})
|
2026-02-21 19:29:34 +00:00
|
|
|
|> assign(:form, to_form(%{}, as: :email))}
|
|
|
|
|
end
|
|
|
|
|
|
2026-03-03 17:41:08 +00:00
|
|
|
@impl true
|
|
|
|
|
def handle_params(params, _uri, socket) do
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
# Support ?adapter=X for no-JS adapter switching
|
|
|
|
|
adapter_key = params["adapter"] || socket.assigns.adapter_key
|
|
|
|
|
|
|
|
|
|
socket =
|
|
|
|
|
if adapter_key && adapter_key != socket.assigns.adapter_key do
|
|
|
|
|
values = load_adapter_values(adapter_key)
|
|
|
|
|
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:adapter_key, adapter_key)
|
|
|
|
|
|> assign(:selected_adapter, Adapters.get(adapter_key))
|
|
|
|
|
|> assign(:current_values, values)
|
|
|
|
|
|> assign(:field_errors, %{})
|
|
|
|
|
|> assign(:test_result, nil)
|
|
|
|
|
|> assign(:test_error, nil)
|
|
|
|
|
else
|
|
|
|
|
socket
|
|
|
|
|
end
|
|
|
|
|
|
2026-03-03 17:41:08 +00:00
|
|
|
{:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
|
|
|
|
|
end
|
|
|
|
|
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
defp load_adapter_values(nil), do: %{}
|
|
|
|
|
|
2026-02-21 19:57:23 +00:00
|
|
|
defp load_adapter_values(adapter_key) do
|
|
|
|
|
case Adapters.get(adapter_key) do
|
|
|
|
|
nil ->
|
|
|
|
|
%{}
|
|
|
|
|
|
|
|
|
|
adapter_info ->
|
|
|
|
|
for field <- adapter_info.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
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
@impl true
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
def handle_event("form_change", %{"email" => %{"adapter" => key}}, socket) do
|
|
|
|
|
if key == socket.assigns.adapter_key do
|
|
|
|
|
{:noreply, socket}
|
|
|
|
|
else
|
|
|
|
|
values = load_adapter_values(key)
|
2026-03-04 12:17:56 +00:00
|
|
|
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:adapter_key, key)
|
|
|
|
|
|> assign(:selected_adapter, Adapters.get(key))
|
|
|
|
|
|> assign(:current_values, values)
|
|
|
|
|
|> assign(:field_errors, %{})
|
|
|
|
|
|> assign(:test_result, nil)
|
|
|
|
|
|> assign(:test_error, nil)}
|
|
|
|
|
end
|
2026-02-21 19:29:34 +00:00
|
|
|
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"]
|
|
|
|
|
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
case Mailer.save_config(adapter_key, params, socket.assigns.current_scope.user.email) do
|
|
|
|
|
{:ok, _adapter_info} ->
|
|
|
|
|
{current_adapter, current_values} = Mailer.current_config()
|
|
|
|
|
|
|
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:adapter_key, current_adapter)
|
|
|
|
|
|> assign(:selected_adapter, Adapters.get(current_adapter))
|
|
|
|
|
|> assign(:current_values, current_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)}
|
2026-02-21 19:29:34 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def handle_event("send_test", _params, socket) do
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
send(self(), :do_send_test)
|
|
|
|
|
{:noreply, assign(socket, :sending_test, true)}
|
|
|
|
|
end
|
2026-02-21 19:29:34 +00:00
|
|
|
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
@impl true
|
|
|
|
|
def handle_info(:do_send_test, socket) do
|
|
|
|
|
user = socket.assigns.current_scope.user
|
2026-02-21 19:29:34 +00:00
|
|
|
|
2026-03-04 17:12:10 +00:00
|
|
|
case Mailer.send_test_email(user.email, Mailer.from_address()) do
|
2026-02-21 19:29:34 +00:00
|
|
|
{:ok, _} ->
|
2026-02-21 22:25:27 +00:00
|
|
|
Mailer.mark_email_verified()
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:sending_test, false)
|
2026-03-04 17:12:10 +00:00
|
|
|
|> assign(:test_result, :ok)
|
|
|
|
|
|> assign(:test_error, nil)}
|
2026-02-21 19:29:34 +00:00
|
|
|
|
|
|
|
|
{:error, reason} ->
|
|
|
|
|
{:noreply,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:sending_test, false)
|
2026-03-04 17:12:10 +00:00
|
|
|
|> assign(:test_result, :error)
|
|
|
|
|
|> assign(:test_error, Mailer.friendly_error(reason))
|
|
|
|
|
|> assign(:test_retryable, Mailer.retryable_error?(reason))}
|
2026-02-21 19:29:34 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
# Swoosh test adapter sends {:email, ...} messages — ignore them
|
|
|
|
|
def handle_info({:email, _}, socket), do: {:noreply, socket}
|
2026-02-21 19:29:34 +00:00
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
|
def render(assigns) do
|
|
|
|
|
~H"""
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-content-medium">
|
2026-03-03 17:41:08 +00:00
|
|
|
<div :if={@from_checklist} class="admin-checklist-banner">
|
|
|
|
|
<.icon name="hero-clipboard-document-check" class="size-5 admin-checklist-banner-icon" />
|
|
|
|
|
<span class="admin-checklist-banner-text">
|
|
|
|
|
You're setting up email for your shop.
|
|
|
|
|
</span>
|
|
|
|
|
<.link navigate={~p"/admin"} class="admin-link admin-checklist-banner-link">
|
|
|
|
|
← Back to checklist
|
|
|
|
|
</.link>
|
|
|
|
|
</div>
|
2026-02-21 19:29:34 +00:00
|
|
|
<.header>
|
|
|
|
|
Email settings
|
|
|
|
|
<:subtitle>
|
2026-03-04 17:12:10 +00:00
|
|
|
Your shop needs an email provider to send order confirmations,
|
|
|
|
|
shipping updates, and newsletters to your customers.
|
2026-02-21 19:29:34 +00:00
|
|
|
</:subtitle>
|
|
|
|
|
</.header>
|
|
|
|
|
|
|
|
|
|
<%= if @env_locked do %>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<div class="admin-callout-warning">
|
|
|
|
|
<div class="admin-callout-warning-body">
|
|
|
|
|
<span class="admin-callout-warning-icon">
|
|
|
|
|
<.icon name="hero-lock-closed" class="size-5" />
|
|
|
|
|
</span>
|
2026-02-21 19:29:34 +00:00
|
|
|
<div>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<p class="admin-callout-warning-title">
|
2026-02-21 19:29:34 +00:00
|
|
|
Controlled by environment variables
|
|
|
|
|
</p>
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<p class="admin-callout-warning-desc">
|
2026-02-21 19:29:34 +00:00
|
|
|
Email is configured via <code>SMTP_HOST</code> and related env vars.
|
|
|
|
|
Remove them to configure email from this page instead.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<% end %>
|
|
|
|
|
|
complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones)
- Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid
primitives and gap variants (sm, md, lg, xl)
- Add transitions.css import and layout.css import to admin.css entry point
- Replace all Tailwind utility classes across 26 admin templates with
semantic admin-*/theme-*/page-specific CSS classes
- Replace all non-dynamic inline styles with semantic classes
- Add ~100 new semantic classes to components.css (analytics, dashboard,
order detail, settings, theme editor, generic utilities)
- Fix stray text-error → admin-text-error in media.ex
- Add missing .truncate definition to admin CSS
- Only remaining inline styles are dynamic data values (progress bars,
chart dimensions) and one JS.toggle target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:40:21 +00:00
|
|
|
<section class="admin-section">
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<.form
|
|
|
|
|
for={@form}
|
|
|
|
|
action={~p"/admin/settings/email"}
|
|
|
|
|
method="post"
|
|
|
|
|
phx-change="form_change"
|
|
|
|
|
phx-submit="save"
|
|
|
|
|
>
|
|
|
|
|
<%!-- Hidden field tracks which adapter's config fields are rendered --%>
|
|
|
|
|
<input
|
|
|
|
|
type="hidden"
|
|
|
|
|
name="email[configured_adapter]"
|
|
|
|
|
value={@adapter_key || ""}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-03-04 17:12:10 +00:00
|
|
|
<%!-- Step 1: Choose a provider --%>
|
|
|
|
|
<div class="admin-setup-step">
|
|
|
|
|
<div class="admin-setup-step-header">
|
|
|
|
|
<span class="admin-setup-step-number">1</span>
|
|
|
|
|
<h2 class="admin-setup-step-title">Choose a provider</h2>
|
|
|
|
|
</div>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
|
|
|
|
|
<fieldset class="card-radio-fieldset" disabled={@env_locked}>
|
|
|
|
|
<legend class="sr-only">Email provider</legend>
|
|
|
|
|
|
|
|
|
|
<h3 class="card-radio-group-heading">Popular providers</h3>
|
|
|
|
|
<p class="card-radio-group-desc">
|
|
|
|
|
Newsletters and transactional emails. All have free tiers.
|
|
|
|
|
<span
|
|
|
|
|
:if={Enum.any?(@all_email_adapters, & &1.recommended)}
|
|
|
|
|
class="card-radio-group-hint"
|
|
|
|
|
>
|
|
|
|
|
Not sure which? Pick the recommended one.
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
<div class="card-radio-grid">
|
|
|
|
|
<.provider_card
|
|
|
|
|
:for={adapter <- @all_email_adapters}
|
|
|
|
|
adapter={adapter}
|
|
|
|
|
selected={@adapter_key}
|
|
|
|
|
disabled={@env_locked}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h3 class="card-radio-group-heading">Transactional only</h3>
|
|
|
|
|
<p class="card-radio-group-desc">
|
|
|
|
|
Order confirmations and shipping updates.
|
|
|
|
|
You'll need a separate service for newsletters later.
|
|
|
|
|
</p>
|
|
|
|
|
<div class="card-radio-grid">
|
|
|
|
|
<.provider_card
|
|
|
|
|
:for={adapter <- @transactional_adapters}
|
|
|
|
|
adapter={adapter}
|
|
|
|
|
selected={@adapter_key}
|
|
|
|
|
disabled={@env_locked}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<details class="admin-provider-other">
|
|
|
|
|
<summary class="admin-provider-other-toggle">
|
|
|
|
|
Already have your own email server?
|
|
|
|
|
</summary>
|
2026-03-04 17:12:10 +00:00
|
|
|
<div class="card-radio-grid">
|
|
|
|
|
<.provider_card
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
:for={adapter <- @advanced_adapters}
|
2026-03-04 17:12:10 +00:00
|
|
|
adapter={adapter}
|
|
|
|
|
selected={@adapter_key}
|
|
|
|
|
disabled={@env_locked}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
</details>
|
|
|
|
|
</fieldset>
|
2026-02-21 19:29:34 +00:00
|
|
|
</div>
|
|
|
|
|
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<%!-- Steps 2 & 3 appear for the selected adapter only --%>
|
|
|
|
|
<div :if={@selected_adapter} class="admin-adapter-config">
|
|
|
|
|
<%!-- Step 2: Create an account (providers with sign-up URLs) --%>
|
|
|
|
|
<div :if={@selected_adapter.url} class="admin-setup-step">
|
|
|
|
|
<div class="admin-setup-step-header">
|
|
|
|
|
<span class="admin-setup-step-number">2</span>
|
|
|
|
|
<h2 class="admin-setup-step-title">Create a free account</h2>
|
2026-02-21 19:29:34 +00:00
|
|
|
</div>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<p class="admin-setup-step-desc">
|
|
|
|
|
<.external_link href={@selected_adapter.url} class="admin-link">
|
|
|
|
|
Sign up at {@selected_adapter.name} ↗
|
|
|
|
|
</.external_link>
|
|
|
|
|
if you don't already have an account. It's free.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-03-04 17:12:10 +00:00
|
|
|
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<%!-- Step 3 (or 2 for advanced): Paste your key --%>
|
|
|
|
|
<div class="admin-setup-step">
|
|
|
|
|
<div class="admin-setup-step-header">
|
|
|
|
|
<span class="admin-setup-step-number">
|
|
|
|
|
{if @selected_adapter.url, do: "3", else: "2"}
|
|
|
|
|
</span>
|
|
|
|
|
<h2 class="admin-setup-step-title">{adapter_fields_title(@selected_adapter)}</h2>
|
2026-03-04 17:12:10 +00:00
|
|
|
</div>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<p class="admin-setup-step-desc">{adapter_fields_instruction(@selected_adapter)}</p>
|
|
|
|
|
<%= for field <- @selected_adapter.fields do %>
|
|
|
|
|
<.adapter_field_static
|
|
|
|
|
field_def={field}
|
|
|
|
|
value={@current_values[field.key]}
|
|
|
|
|
disabled={@env_locked}
|
|
|
|
|
error={@field_errors[field.key]}
|
|
|
|
|
/>
|
|
|
|
|
<% end %>
|
|
|
|
|
<%= unless @env_locked do %>
|
|
|
|
|
<div class="admin-row admin-row-lg">
|
|
|
|
|
<.button phx-disable-with="Saving...">
|
|
|
|
|
Save settings
|
|
|
|
|
</.button>
|
|
|
|
|
</div>
|
|
|
|
|
<% end %>
|
2026-02-21 19:29:34 +00:00
|
|
|
</div>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<%!-- No-JS: if adapter changed but config not yet shown, submit to reload --%>
|
|
|
|
|
<noscript>
|
|
|
|
|
<div :if={!@selected_adapter} class="admin-row admin-row-lg" style="margin-top: 1rem;">
|
|
|
|
|
<.button>Continue</.button>
|
|
|
|
|
</div>
|
|
|
|
|
</noscript>
|
2026-02-21 19:29:34 +00:00
|
|
|
</.form>
|
|
|
|
|
</section>
|
|
|
|
|
|
2026-03-04 17:12:10 +00:00
|
|
|
<%!-- Step 4: Send a test email (only after config saved) --%>
|
|
|
|
|
<div :if={@email_configured} class="admin-setup-step" style="margin-top: 1.5rem;">
|
|
|
|
|
<div class="admin-setup-step-header">
|
|
|
|
|
<span class={[
|
|
|
|
|
"admin-setup-step-number",
|
|
|
|
|
@test_result == :ok && "admin-setup-step-number-done",
|
|
|
|
|
@test_result == :error && "admin-setup-step-number-error"
|
|
|
|
|
]}>
|
|
|
|
|
<%= 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 %>
|
|
|
|
|
</span>
|
|
|
|
|
<h2 class="admin-setup-step-title">
|
|
|
|
|
<%= cond do %>
|
|
|
|
|
<% @test_result == :ok -> %>
|
|
|
|
|
Email is working
|
|
|
|
|
<% @test_result == :error -> %>
|
|
|
|
|
Test failed
|
|
|
|
|
<% true -> %>
|
|
|
|
|
Send a test email
|
|
|
|
|
<% end %>
|
|
|
|
|
</h2>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<%= if @test_result == :ok do %>
|
|
|
|
|
<p class="admin-setup-step-desc">
|
|
|
|
|
Test email sent to <strong>{@current_scope.user.email}</strong>.
|
|
|
|
|
Check your inbox to confirm it arrived.
|
2026-02-21 19:29:34 +00:00
|
|
|
</p>
|
2026-03-04 17:12:10 +00:00
|
|
|
<div class="admin-row admin-row-sm">
|
|
|
|
|
<.link
|
|
|
|
|
:if={@from_checklist}
|
|
|
|
|
navigate={~p"/admin"}
|
|
|
|
|
class="admin-btn admin-btn-primary admin-btn-sm"
|
2026-02-21 19:29:34 +00:00
|
|
|
>
|
2026-03-04 17:12:10 +00:00
|
|
|
Continue setup →
|
|
|
|
|
</.link>
|
|
|
|
|
<.button type="button" phx-click="send_test">
|
|
|
|
|
Send again
|
|
|
|
|
</.button>
|
2026-02-21 19:29:34 +00:00
|
|
|
</div>
|
2026-03-04 17:12:10 +00:00
|
|
|
<% else %>
|
|
|
|
|
<%= if @test_result == :error do %>
|
|
|
|
|
<p class="admin-test-error">{@test_error}</p>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<div class="admin-row admin-row-sm">
|
2026-03-04 17:12:10 +00:00
|
|
|
<%= if @test_retryable do %>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<.button
|
|
|
|
|
type="button"
|
2026-03-04 17:12:10 +00:00
|
|
|
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"}
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
</.button>
|
2026-03-04 17:12:10 +00:00
|
|
|
<% else %>
|
|
|
|
|
<p class="admin-setup-step-desc">
|
|
|
|
|
Fix your settings above and reconnect, then try the test again.
|
|
|
|
|
</p>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
|
|
|
|
<% else %>
|
|
|
|
|
<p class="admin-setup-step-desc">
|
|
|
|
|
Send a test to <strong>{@current_scope.user.email}</strong> to check everything works.
|
|
|
|
|
</p>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<div class="admin-row admin-row-sm">
|
|
|
|
|
<.button
|
|
|
|
|
type="button"
|
2026-03-04 17:12:10 +00:00
|
|
|
phx-click="send_test"
|
|
|
|
|
disabled={@sending_test}
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
phx-disable-with="Sending..."
|
2026-03-04 17:12:10 +00:00
|
|
|
>
|
fix email settings: missing providers, a11y, no-JS support
show all 10 providers in three groups (popular, transactional,
advanced) with category headings. fix phx-change clobbering text
fields, async test email sending state, integer parse crash on
bad port. add keyboard focus on card radios, fieldset legend,
WCAG-compliant badge contrast, responsive grid. extract shared
save_config into Mailer, add no-JS controller fallback with
configured_adapter hidden field for adapter change detection.
remove CardRadioScroll JS hook (no longer needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:26:59 +00:00
|
|
|
<.icon name="hero-paper-airplane" class="size-4" /> Send test email
|
|
|
|
|
</.button>
|
|
|
|
|
<%!-- No-JS fallback for test email --%>
|
|
|
|
|
<noscript>
|
|
|
|
|
<.form
|
|
|
|
|
for={%{}}
|
|
|
|
|
action={~p"/admin/settings/email/test"}
|
|
|
|
|
method="post"
|
|
|
|
|
style="display:inline"
|
|
|
|
|
>
|
|
|
|
|
<.button>Send test email</.button>
|
|
|
|
|
</.form>
|
|
|
|
|
</noscript>
|
2026-03-04 17:12:10 +00:00
|
|
|
</div>
|
|
|
|
|
<% end %>
|
|
|
|
|
<% end %>
|
|
|
|
|
</div>
|
2026-02-21 19:29:34 +00:00
|
|
|
</div>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
2026-03-04 17:12:10 +00:00
|
|
|
# ── Local components ──
|
|
|
|
|
|
|
|
|
|
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"""
|
|
|
|
|
<label class={[
|
|
|
|
|
"card-radio-card",
|
|
|
|
|
@selected == @adapter.key && "card-radio-card-selected"
|
|
|
|
|
]}>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
id={"email-adapter-#{@adapter.key}"}
|
|
|
|
|
name="email[adapter]"
|
|
|
|
|
value={@adapter.key}
|
|
|
|
|
checked={@selected == @adapter.key}
|
|
|
|
|
disabled={@disabled}
|
|
|
|
|
class="card-radio-input"
|
|
|
|
|
/>
|
|
|
|
|
<span class="card-radio-name">
|
|
|
|
|
{@adapter.name}
|
|
|
|
|
<span :if={@adapter.recommended} class="card-radio-badge card-radio-recommended">
|
|
|
|
|
Recommended
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span :if={@adapter.free_tier} class="card-radio-description">{@adapter.free_tier}</span>
|
|
|
|
|
<span :if={@adapter.setup_hint} class="card-radio-description">{@adapter.setup_hint}</span>
|
|
|
|
|
</label>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# ── Field renderers ──
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
attr :field_def, :map, required: true
|
|
|
|
|
attr :value, :any, default: nil
|
|
|
|
|
attr :disabled, :boolean, default: false
|
2026-03-04 12:17:56 +00:00
|
|
|
attr :error, :string, default: nil
|
2026-02-21 19:29:34 +00:00
|
|
|
|
|
|
|
|
defp adapter_field_static(%{field_def: %{type: :secret}} = assigns) do
|
2026-03-04 12:17:56 +00:00
|
|
|
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
~H"""
|
2026-03-04 17:12:10 +00:00
|
|
|
<.input
|
|
|
|
|
name={"email[#{@field_def.key}]"}
|
|
|
|
|
value=""
|
|
|
|
|
type="text"
|
|
|
|
|
label={@field_def.label}
|
|
|
|
|
autocomplete="off"
|
|
|
|
|
placeholder={@value || ""}
|
|
|
|
|
disabled={@disabled}
|
|
|
|
|
errors={@errors}
|
|
|
|
|
/>
|
2026-02-21 19:29:34 +00:00
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp adapter_field_static(%{field_def: %{type: :integer}} = assigns) do
|
2026-03-04 12:17:56 +00:00
|
|
|
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
~H"""
|
|
|
|
|
<.input
|
|
|
|
|
name={"email[#{@field_def.key}]"}
|
|
|
|
|
value={@value || @field_def.default || ""}
|
|
|
|
|
type="number"
|
|
|
|
|
label={@field_def.label}
|
|
|
|
|
disabled={@disabled}
|
2026-03-04 12:17:56 +00:00
|
|
|
errors={@errors}
|
2026-02-21 19:29:34 +00:00
|
|
|
/>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp adapter_field_static(assigns) do
|
2026-03-04 12:17:56 +00:00
|
|
|
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
|
|
|
|
|
|
2026-02-21 19:29:34 +00:00
|
|
|
~H"""
|
|
|
|
|
<.input
|
|
|
|
|
name={"email[#{@field_def.key}]"}
|
|
|
|
|
value={@value || @field_def.default || ""}
|
|
|
|
|
type="text"
|
|
|
|
|
label={@field_def.label}
|
|
|
|
|
disabled={@disabled}
|
2026-03-04 12:17:56 +00:00
|
|
|
errors={@errors}
|
2026-02-21 19:29:34 +00:00
|
|
|
/>
|
|
|
|
|
"""
|
|
|
|
|
end
|
|
|
|
|
end
|