rework email settings for true progressive enhancement
All checks were successful
deploy / deploy (push) Successful in 1m19s

Render all adapter field sections in the form with CSS :has(:checked)
controlling visibility. Selecting a provider instantly shows its config
fields — no JS, no page reload, no server round-trip needed.

- Render all 6 adapter configs with data-adapter attribute
- CSS :has(:checked) show/hide rules per adapter in admin stylesheet
- Namespace field names per adapter (email[brevo][api_key] etc)
- Drop 4 transactional-only providers (Resend, Postmark, Mailgun, MailPace)
- Remove noscript "Switch provider" button and controller redirect workaround
- Remove configured_adapter hidden input tracking
- Hide JS-only test email button for no-JS users via noscript style
- LiveView progressively enhances with async save and test email

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-04 23:10:37 +00:00
parent dd20ea824f
commit db130a7155
12 changed files with 213 additions and 456 deletions

View File

@ -24,7 +24,7 @@ Tier 1 MVP complete. Tier 2 production readiness complete (except Litestream and
- Abandoned cart recovery (GDPR-compliant, single email) - Abandoned cart recovery (GDPR-compliant, single email)
- Favicon generation from source image (PNG variants, SVG dark mode, webmanifest) - Favicon generation from source image (PNG variants, SVG dark mode, webmanifest)
- Complete SEO (OG/Twitter cards, JSON-LD, sitemap, canonical URLs, meta descriptions) - Complete SEO (OG/Twitter cards, JSON-LD, sitemap, canonical URLs, meta descriptions)
- Email settings admin with 10 adapter options and test email - Email settings admin with 6 adapter options, no-JS support, and test email
- No-JS support across all key flows - No-JS support across all key flows
- Fully Tailwind-free CSS (12 KB gzipped shop+theme, 95 KB gzipped admin total) - Fully Tailwind-free CSS (12 KB gzipped shop+theme, 95 KB gzipped admin total)
- CI pipeline (compile warnings, format, credo, dialyzer, tests) - CI pipeline (compile warnings, format, credo, dialyzer, tests)
@ -60,7 +60,7 @@ Based on usability testing (March 2026). Reworks the entire setup flow into a si
| A | Simplify initial setup to account creation only (email, password, shop name) | 1.5h | planned | | A | Simplify initial setup to account creation only (email, password, shop name) | 1.5h | planned |
| B | Guided setup flow with progress bar (multi-step, skippable, explains "why") | 4h | planned | | B | Guided setup flow with progress bar (multi-step, skippable, explains "why") | 4h | planned |
| C | Forgiving API key validation (strip whitespace, format checks, helpful errors) | 1.5h | done | | C | Forgiving API key validation (strip whitespace, format checks, helpful errors) | 1.5h | done |
| D | Email provider setup UX rework (recommended pick, grouping, guided flow, test email) | 2h | planned | | D | Email provider setup UX rework (6 adapters, no-JS, `:has(:checked)` CSS, test email) | 2h | done |
| E | Contextual prompts for skipped steps (products, checkout, order detail) | 2h | done | | E | Contextual prompts for skipped steps (products, checkout, order detail) | 2h | done |
| F | Dashboard checklist and messaging rework | 2h | planned | | F | Dashboard checklist and messaging rework | 2h | planned |
| G | Coming soon page fixes (logo layout, admin login link) | 30m | done | | G | Coming soon page fixes (logo layout, admin login link) | 30m | done |

View File

@ -1492,7 +1492,7 @@
outline-offset: 2px; outline-offset: 2px;
} }
&.card-radio-card-selected { &:has(:checked) {
border-color: var(--t-text-primary, #171717); border-color: var(--t-text-primary, #171717);
background: var(--t-surface-sunken, #e5e5e5); background: var(--t-surface-sunken, #e5e5e5);
} }
@ -4379,6 +4379,20 @@
gap: 1.5rem; gap: 1.5rem;
} }
/* Show only the adapter config matching the checked radio */
.admin-adapter-config[data-adapter] { display: none; }
.admin-content-medium:has(#email-adapter-brevo:checked) [data-adapter="brevo"],
.admin-content-medium:has(#email-adapter-sendgrid:checked) [data-adapter="sendgrid"],
.admin-content-medium:has(#email-adapter-mailjet:checked) [data-adapter="mailjet"],
.admin-content-medium:has(#email-adapter-mailersend:checked) [data-adapter="mailersend"],
.admin-content-medium:has(#email-adapter-smtp:checked) [data-adapter="smtp"],
.admin-content-medium:has(#email-adapter-postal:checked) [data-adapter="postal"] {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* ── Campaign form ── */ /* ── Campaign form ── */
.admin-campaign-actions { .admin-campaign-actions {

View File

@ -1,6 +1,6 @@
# Onboarding UX v2 # Onboarding UX v2
Status: Planned Status: In progress
Supersedes the original onboarding-ux plan. Based on usability testing session (March 2026) covering the setup wizard, launch checklist, email provider setup, and general onboarding flow. Supersedes the original onboarding-ux plan. Based on usability testing session (March 2026) covering the setup wizard, launch checklist, email provider setup, and general onboarding flow.
@ -77,20 +77,31 @@ API key entry should be as forgiving as possible:
**Files:** `lib/berrypod_web/live/setup/onboarding.ex`, `lib/berrypod_web/live/admin/providers.ex`, validation logic in contexts **Files:** `lib/berrypod_web/live/setup/onboarding.ex`, `lib/berrypod_web/live/admin/providers.ex`, validation logic in contexts
### D. Email provider setup UX ### D. Email provider setup UX — done
Rework the email provider selection and configuration: Reworked the email provider selection and configuration. Changes across two sessions:
- **Recommended pick** highlighted at top — whichever has the best free tier, easiest setup, and supports newsletters. One clear "if you're not sure, pick this" option. **Session 1 (commit dd659e4):**
- **Two groups:** "Also sends newsletters" vs "Transactional only" with one-line explanation of the difference - Recommended pick (Brevo) highlighted with badge
- **One-liner per provider:** free tier info, setup difficulty ("paste one API key" vs "requires domain verification") - Two groups: "Also sends newsletters" vs "Transactional only"
- **Self-hosted/SMTP** in a collapsible "Advanced" section at bottom - One-liner per provider with free tier info
- **Guided flow after selection:** link to create account (new tab), then "Now enter these settings:" with config fields - Self-hosted/SMTP in collapsible "Advanced" section
- **Send test email** button after configuration - Guided flow after selection with config fields
- **Remove** the masked key display ("Current: 84159e26•••4f3") - Send test email button with async delivery
- **Default from address** to the admin email automatically — don't surface during setup, it's a general settings thing for later - Removed masked key display
- Default from address auto-set from admin email
- No-JS support via controller POST fallback
- 17 audit fixes (a11y, error handling, UX)
**Files:** `lib/berrypod_web/live/admin/email_settings.ex`, shared components for guided flow **Session 2 (uncommitted):**
- Dropped 4 transactional-only providers (Resend, Postmark, Mailgun, MailPace) — all are transactional-only and not useful for a shop that needs newsletters. 6 adapters remain: Brevo, SendGrid, Mailjet, MailerSend + SMTP, Postal
- Removed "Popular providers" heading and description (unnecessary with fewer providers)
- Fixed no-JS provider switching: noscript submit button always visible with dynamic text
- Fixed no-JS radio highlight: replaced server-applied `card-radio-card-selected` class with pure CSS `:has(:checked)` selector
- Cleaned up key validation (removed 4 provider clauses, removed 2 known prefixes)
- Updated all related tests (adapters, key validation, email settings LiveView, controller, mailer)
**Files:** `lib/berrypod_web/live/admin/email_settings.ex`, `lib/berrypod/mailer/adapters.ex`, `lib/berrypod/key_validation.ex`, `assets/css/admin/components.css`, `lib/berrypod_web/controllers/email_settings_controller.ex`
### E. Contextual prompts for skipped steps ### E. Contextual prompts for skipped steps
@ -148,7 +159,7 @@ Increase input field border contrast to meet WCAG AA (3:1 minimum for UI compone
| A | Simplify initial setup to account creation only | 1.5h | planned | | A | Simplify initial setup to account creation only | 1.5h | planned |
| B | Guided setup flow with progress bar | 4h | planned | | B | Guided setup flow with progress bar | 4h | planned |
| C | Forgiving API key validation | 1.5h | planned | | C | Forgiving API key validation | 1.5h | planned |
| D | Email provider setup UX rework | 2h | planned | | D | Email provider setup UX rework | 2h | done |
| E | Contextual prompts for skipped steps | 2h | done | | E | Contextual prompts for skipped steps | 2h | done |
| F | Dashboard checklist and messaging rework | 2h | planned | | F | Dashboard checklist and messaging rework | 2h | planned |
| G | Coming soon page fixes (logo + admin link) | 30m | planned | | G | Coming soon page fixes (logo + admin link) | 30m | planned |

View File

@ -84,8 +84,6 @@ defmodule Berrypod.KeyValidation do
@known_prefixes [ @known_prefixes [
{"SG.", "SendGrid"}, {"SG.", "SendGrid"},
{"xkeysib-", "Brevo"}, {"xkeysib-", "Brevo"},
{"re_", "Resend"},
{"key-", "Mailgun"},
{"mlsn.", "MailerSend"} {"mlsn.", "MailerSend"}
] ]
@ -98,56 +96,6 @@ defmodule Berrypod.KeyValidation do
end end
end end
# Postmark: UUID format
defp validate_email_format(key, "postmark", "api_key") do
uuid_pattern = ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
cond do
Regex.match?(uuid_pattern, key) ->
{:ok, key}
wrong = wrong_provider_hint(key, "postmark") ->
{:error, wrong}
true ->
{:error,
"Postmark server tokens are UUIDs (like abc12345-abcd-1234-abcd-123456789abc). Make sure you copy the server token, not the account ID"}
end
end
# Resend: re_ prefix
defp validate_email_format(key, "resend", "api_key") do
cond do
String.starts_with?(key, "re_") ->
{:ok, key}
wrong = wrong_provider_hint(key, "resend") ->
{:error, wrong}
true ->
{:error,
"Resend API keys start with re_ — find yours in your Resend dashboard under API Keys"}
end
end
# Mailgun: key- prefix (classic) or long RBAC keys
defp validate_email_format(key, "mailgun", "api_key") do
cond do
String.starts_with?(key, "key-") ->
{:ok, key}
String.length(key) >= 20 ->
{:ok, key}
wrong = wrong_provider_hint(key, "mailgun") ->
{:error, wrong}
true ->
{:error,
"Mailgun API keys usually start with key- — find yours in Settings → API Security"}
end
end
# Brevo: xkeysib- prefix # Brevo: xkeysib- prefix
defp validate_email_format(key, "brevo", "api_key") do defp validate_email_format(key, "brevo", "api_key") do
cond do cond do
@ -210,21 +158,6 @@ defmodule Berrypod.KeyValidation do
end end
end end
# MailPace: server token, no known prefix
defp validate_email_format(key, "mailpace", "api_key") do
cond do
String.length(key) >= 10 ->
{:ok, key}
wrong = wrong_provider_hint(key, "mailpace") ->
{:error, wrong}
true ->
{:error,
"This looks too short — find your server API token under your domain settings in MailPace"}
end
end
# Non-api_key fields (domain, relay, base_url, etc.), basic checks # Non-api_key fields (domain, relay, base_url, etc.), basic checks
defp validate_email_format(key, _adapter_key, _field_key) do defp validate_email_format(key, _adapter_key, _field_key) do
if String.length(key) < 3 do if String.length(key) < 3 do

View File

@ -69,64 +69,6 @@ defmodule Berrypod.Mailer.Adapters do
%Field{key: "api_key", label: "API key", type: :secret, required: true} %Field{key: "api_key", label: "API key", type: :secret, required: true}
] ]
}, },
# ── Transactional only ──
%Adapter{
key: "resend",
name: "Resend",
module: Swoosh.Adapters.Resend,
description: "Developer-friendly API, simple setup.",
tags: ["Transactional", "US"],
category: :transactional,
free_tier: "3,000 emails/month free",
setup_hint: "Paste one API key",
url: "https://resend.com",
fields: [
%Field{key: "api_key", label: "API key", type: :secret, required: true}
]
},
%Adapter{
key: "postmark",
name: "Postmark",
module: Swoosh.Adapters.Postmark,
description: "Excellent deliverability tracking.",
tags: ["Transactional", "US"],
category: :transactional,
free_tier: "100 emails/month free",
setup_hint: "Paste one API key",
url: "https://postmarkapp.com",
fields: [
%Field{key: "api_key", label: "API key", type: :secret, required: true}
]
},
%Adapter{
key: "mailgun",
name: "Mailgun",
module: Swoosh.Adapters.Mailgun,
description: "EU region option available.",
tags: ["Transactional", "EU option", "Sweden"],
category: :transactional,
free_tier: "100 emails/day trial",
setup_hint: "API key + domain name",
url: "https://www.mailgun.com",
fields: [
%Field{key: "api_key", label: "API key", type: :secret, required: true},
%Field{key: "domain", label: "Domain", type: :string, required: true}
]
},
%Adapter{
key: "mailpace",
name: "MailPace",
module: Swoosh.Adapters.MailPace,
description: "Privacy-focused, simple API.",
tags: ["Transactional", "UK"],
category: :transactional,
free_tier: "3,000 emails/month free",
setup_hint: "Paste one API key",
url: "https://mailpace.com",
fields: [
%Field{key: "api_key", label: "API key", type: :secret, required: true}
]
},
# ── Advanced ── # ── Advanced ──
%Adapter{ %Adapter{
key: "smtp", key: "smtp",

View File

@ -10,27 +10,22 @@ defmodule BerrypodWeb.EmailSettingsController do
alias Berrypod.Mailer alias Berrypod.Mailer
def update(conn, %{"email" => params}) do def update(conn, %{"email" => params}) do
selected = params["adapter"] adapter_key = params["adapter"]
configured = params["configured_adapter"] # Fields are namespaced: email[brevo][api_key] → params["brevo"]["api_key"]
adapter_params = params[adapter_key] || %{}
if selected != configured do case Mailer.save_config(adapter_key, adapter_params, conn.assigns.current_scope.user.email) do
# User changed adapter radio but config fields are for the old adapter. {:ok, _adapter_info} ->
# Redirect to show the new adapter's config fields. conn
redirect(conn, to: ~p"/admin/settings/email?adapter=#{selected}") |> put_flash(:info, "Settings saved — send a test email to check it works")
else |> redirect(to: ~p"/admin/settings/email")
case Mailer.save_config(selected, params, conn.assigns.current_scope.user.email) do
{:ok, _adapter_info} ->
conn
|> put_flash(:info, "Settings saved — send a test email to check it works")
|> redirect(to: ~p"/admin/settings/email")
{:error, field_errors} when is_map(field_errors) -> {:error, field_errors} when is_map(field_errors) ->
message = field_errors |> Map.values() |> Enum.join(". ") message = field_errors |> Map.values() |> Enum.join(". ")
conn conn
|> put_flash(:error, message) |> put_flash(:error, message)
|> redirect(to: ~p"/admin/settings/email?adapter=#{selected}") |> redirect(to: ~p"/admin/settings/email")
end
end end
end end

View File

@ -8,21 +8,23 @@ defmodule BerrypodWeb.Admin.EmailSettings do
@impl true @impl true
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
env_locked = Mailer.env_var_configured?() env_locked = Mailer.env_var_configured?()
{current_adapter, current_values} = Mailer.current_config() {current_adapter, _current_values} = Mailer.current_config()
saved_adapter = Settings.get_setting("email_adapter") saved_adapter = Settings.get_setting("email_adapter")
adapter_key = current_adapter || saved_adapter adapter_key = current_adapter || saved_adapter
grouped = Adapters.grouped() grouped = Adapters.grouped()
all_adapters = Adapters.all()
all_values = load_all_adapter_values()
{:ok, {:ok,
socket socket
|> assign(:page_title, "Email settings") |> assign(:page_title, "Email settings")
|> assign(:env_locked, env_locked) |> assign(:env_locked, env_locked)
|> assign(:adapter_key, adapter_key) |> assign(:adapter_key, adapter_key)
|> assign(:current_values, current_values) |> assign(:all_values, all_values)
|> assign(:all_email_adapters, grouped[:all_email] || []) |> assign(:all_email_adapters, grouped[:all_email] || [])
|> assign(:transactional_adapters, grouped[:transactional] || [])
|> assign(:advanced_adapters, grouped[:advanced] || []) |> assign(:advanced_adapters, grouped[:advanced] || [])
|> assign(:all_adapters, all_adapters)
|> assign(:email_configured, Mailer.email_configured?()) |> assign(:email_configured, Mailer.email_configured?())
|> assign(:selected_adapter, adapter_key && Adapters.get(adapter_key)) |> assign(:selected_adapter, adapter_key && Adapters.get(adapter_key))
|> assign(:sending_test, false) |> assign(:sending_test, false)
@ -36,37 +38,14 @@ defmodule BerrypodWeb.Admin.EmailSettings do
@impl true @impl true
def handle_params(params, _uri, socket) do def handle_params(params, _uri, socket) do
# 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
{:noreply, assign(socket, :from_checklist, params["from"] == "checklist")} {:noreply, assign(socket, :from_checklist, params["from"] == "checklist")}
end end
defp load_adapter_values(nil), do: %{} defp load_all_adapter_values do
for adapter <- Adapters.all(), into: %{} do
defp load_adapter_values(adapter_key) do values =
case Adapters.get(adapter_key) do for field <- adapter.fields, into: %{} do
nil -> settings_key = Adapters.settings_key(adapter.key, field.key)
%{}
adapter_info ->
for field <- adapter_info.fields, into: %{} do
settings_key = Adapters.settings_key(adapter_key, field.key)
value = value =
case field.type do case field.type do
@ -76,6 +55,8 @@ defmodule BerrypodWeb.Admin.EmailSettings do
{field.key, value} {field.key, value}
end end
{adapter.key, values}
end end
end end
@ -84,13 +65,10 @@ defmodule BerrypodWeb.Admin.EmailSettings do
if key == socket.assigns.adapter_key do if key == socket.assigns.adapter_key do
{:noreply, socket} {:noreply, socket}
else else
values = load_adapter_values(key)
{:noreply, {:noreply,
socket socket
|> assign(:adapter_key, key) |> assign(:adapter_key, key)
|> assign(:selected_adapter, Adapters.get(key)) |> assign(:selected_adapter, Adapters.get(key))
|> assign(:current_values, values)
|> assign(:field_errors, %{}) |> assign(:field_errors, %{})
|> assign(:test_result, nil) |> assign(:test_result, nil)
|> assign(:test_error, nil)} |> assign(:test_error, nil)}
@ -102,16 +80,20 @@ defmodule BerrypodWeb.Admin.EmailSettings do
{:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")} {:noreply, put_flash(socket, :error, "Email config is controlled by environment variables")}
else else
adapter_key = params["adapter"] 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, params, socket.assigns.current_scope.user.email) do case Mailer.save_config(
adapter_key,
adapter_params,
socket.assigns.current_scope.user.email
) do
{:ok, _adapter_info} -> {:ok, _adapter_info} ->
{current_adapter, current_values} = Mailer.current_config()
{:noreply, {:noreply,
socket socket
|> assign(:adapter_key, current_adapter) |> assign(:adapter_key, adapter_key)
|> assign(:selected_adapter, Adapters.get(current_adapter)) |> assign(:selected_adapter, Adapters.get(adapter_key))
|> assign(:current_values, current_values) |> assign(:all_values, load_all_adapter_values())
|> assign(:email_configured, Mailer.email_configured?()) |> assign(:email_configured, Mailer.email_configured?())
|> assign(:field_errors, %{}) |> assign(:field_errors, %{})
|> assign(:test_result, nil) |> assign(:test_result, nil)
@ -204,13 +186,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
phx-change="form_change" phx-change="form_change"
phx-submit="save" phx-submit="save"
> >
<%!-- Hidden field tracks which adapter's config fields are rendered --%>
<input
type="hidden"
name="email[configured_adapter]"
value={@adapter_key || ""}
/>
<%!-- Step 1: Choose a provider --%> <%!-- Step 1: Choose a provider --%>
<div class="admin-setup-step"> <div class="admin-setup-step">
<div class="admin-setup-step-header"> <div class="admin-setup-step-header">
@ -221,16 +196,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
<fieldset class="card-radio-fieldset" disabled={@env_locked}> <fieldset class="card-radio-fieldset" disabled={@env_locked}>
<legend class="sr-only">Email provider</legend> <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"> <div class="card-radio-grid">
<.provider_card <.provider_card
:for={adapter <- @all_email_adapters} :for={adapter <- @all_email_adapters}
@ -240,20 +205,6 @@ defmodule BerrypodWeb.Admin.EmailSettings do
/> />
</div> </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"> <details class="admin-provider-other">
<summary class="admin-provider-other-toggle"> <summary class="admin-provider-other-toggle">
Already have your own email server? Already have your own email server?
@ -270,60 +221,24 @@ defmodule BerrypodWeb.Admin.EmailSettings do
</fieldset> </fieldset>
</div> </div>
<%!-- Steps 2 & 3 appear for the selected adapter only --%> <%!-- Steps 2 & 3 for each adapter (CSS :has(:checked) shows the active one) --%>
<div :if={@selected_adapter} class="admin-adapter-config"> <.adapter_config
<%!-- Step 2: Create an account (providers with sign-up URLs) --%> :for={adapter <- @all_adapters}
<div :if={@selected_adapter.url} class="admin-setup-step"> adapter={adapter}
<div class="admin-setup-step-header"> values={@all_values[adapter.key] || %{}}
<span class="admin-setup-step-number">2</span> field_errors={if(@adapter_key == adapter.key, do: @field_errors, else: %{})}
<h2 class="admin-setup-step-title">Create a free account</h2> env_locked={@env_locked}
</div> />
<p class="admin-setup-step-desc">
<.external_link href={@selected_adapter.url} class="admin-link">
Sign up at {@selected_adapter.name} &nearr;
</.external_link>
if you don't already have an account. It's free.
</p>
</div>
<%!-- 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>
</div>
<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 %>
</div>
</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>
</.form> </.form>
</section> </section>
<%!-- Step 4: Send a test email (only after config saved) --%> <%!-- Step 4: Send a test email (only after config saved) --%>
<div :if={@email_configured} class="admin-setup-step" style="margin-top: 1.5rem;"> <div
:if={@email_configured}
id="test-email-step"
class="admin-setup-step"
style="margin-top: 1.5rem;"
>
<div class="admin-setup-step-header"> <div class="admin-setup-step-header">
<span class={[ <span class={[
"admin-setup-step-number", "admin-setup-step-number",
@ -393,23 +308,31 @@ defmodule BerrypodWeb.Admin.EmailSettings do
Send a test to <strong>{@current_scope.user.email}</strong> to check everything works. Send a test to <strong>{@current_scope.user.email}</strong> to check everything works.
</p> </p>
<div class="admin-row admin-row-sm"> <div class="admin-row admin-row-sm">
<.button <%!-- JS: async send via LiveView --%>
type="button" <span id="test-email-js">
phx-click="send_test" <.button
disabled={@sending_test} type="button"
phx-disable-with="Sending..." phx-click="send_test"
> disabled={@sending_test}
<.icon name="hero-paper-airplane" class="size-4" /> Send test email phx-disable-with="Sending..."
</.button> >
<%!-- No-JS fallback for test email --%> <.icon name="hero-paper-airplane" class="size-4" /> Send test email
</.button>
</span>
<%!-- No-JS: form POST fallback (hides the JS button above) --%>
<noscript> <noscript>
<style>
#test-email-js { display: none; }
</style>
<.form <.form
for={%{}} for={%{}}
action={~p"/admin/settings/email/test"} action={~p"/admin/settings/email/test"}
method="post" method="post"
style="display:inline" style="display:inline"
> >
<.button>Send test email</.button> <.button>
<.icon name="hero-paper-airplane" class="size-4" /> Send test email
</.button>
</.form> </.form>
</noscript> </noscript>
</div> </div>
@ -422,6 +345,58 @@ defmodule BerrypodWeb.Admin.EmailSettings do
# ── Local components ── # ── Local components ──
attr :adapter, :map, required: true
attr :values, :map, required: true
attr :field_errors, :map, required: true
attr :env_locked, :boolean, required: true
defp adapter_config(assigns) do
~H"""
<div class="admin-adapter-config" data-adapter={@adapter.key}>
<%!-- Create an account (providers with sign-up URLs) --%>
<div :if={@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>
</div>
<p class="admin-setup-step-desc">
<.external_link href={@adapter.url} class="admin-link">
Sign up at {@adapter.name} &nearr;
</.external_link>
if you don't already have an account. It's free.
</p>
</div>
<%!-- Paste your key / server details --%>
<div class="admin-setup-step">
<div class="admin-setup-step-header">
<span class="admin-setup-step-number">
{if @adapter.url, do: "3", else: "2"}
</span>
<h2 class="admin-setup-step-title">{adapter_fields_title(@adapter)}</h2>
</div>
<p class="admin-setup-step-desc">{adapter_fields_instruction(@adapter)}</p>
<%= 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 %>
<div class="admin-row admin-row-lg">
<.button phx-disable-with="Saving...">
Save settings
</.button>
</div>
<% end %>
</div>
</div>
"""
end
defp adapter_fields_title(%{key: "smtp"}), do: "Enter your server details" 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: "postal"}), do: "Enter your server details"
defp adapter_fields_title(%{key: "mailjet"}), do: "Paste your API keys" defp adapter_fields_title(%{key: "mailjet"}), do: "Paste your API keys"
@ -448,10 +423,7 @@ defmodule BerrypodWeb.Admin.EmailSettings do
defp provider_card(assigns) do defp provider_card(assigns) do
~H""" ~H"""
<label class={[ <label class="card-radio-card">
"card-radio-card",
@selected == @adapter.key && "card-radio-card-selected"
]}>
<input <input
type="radio" type="radio"
id={"email-adapter-#{@adapter.key}"} id={"email-adapter-#{@adapter.key}"}
@ -474,18 +446,20 @@ defmodule BerrypodWeb.Admin.EmailSettings do
end end
# ── Field renderers ── # ── 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 :field_def, :map, required: true
attr :value, :any, default: nil attr :value, :any, default: nil
attr :disabled, :boolean, default: false attr :disabled, :boolean, default: false
attr :error, :string, default: nil attr :error, :string, default: nil
defp adapter_field_static(%{field_def: %{type: :secret}} = assigns) do defp adapter_field_input(%{field_def: %{type: :secret}} = assigns) do
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: [])) assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
~H""" ~H"""
<.input <.input
name={"email[#{@field_def.key}]"} name={"email[#{@adapter_key}][#{@field_def.key}]"}
value="" value=""
type="text" type="text"
label={@field_def.label} label={@field_def.label}
@ -497,12 +471,12 @@ defmodule BerrypodWeb.Admin.EmailSettings do
""" """
end end
defp adapter_field_static(%{field_def: %{type: :integer}} = assigns) do defp adapter_field_input(%{field_def: %{type: :integer}} = assigns) do
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: [])) assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
~H""" ~H"""
<.input <.input
name={"email[#{@field_def.key}]"} name={"email[#{@adapter_key}][#{@field_def.key}]"}
value={@value || @field_def.default || ""} value={@value || @field_def.default || ""}
type="number" type="number"
label={@field_def.label} label={@field_def.label}
@ -512,12 +486,12 @@ defmodule BerrypodWeb.Admin.EmailSettings do
""" """
end end
defp adapter_field_static(assigns) do defp adapter_field_input(assigns) do
assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: [])) assigns = assign(assigns, :errors, if(assigns.error, do: [assigns.error], else: []))
~H""" ~H"""
<.input <.input
name={"email[#{@field_def.key}]"} name={"email[#{@adapter_key}][#{@field_def.key}]"}
value={@value || @field_def.default || ""} value={@value || @field_def.default || ""}
type="text" type="text"
label={@field_def.label} label={@field_def.label}

View File

@ -112,52 +112,6 @@ defmodule Berrypod.KeyValidationTest do
end end
end end
describe "validate_email_key/3 - Postmark" do
test "accepts valid UUID" do
key = "abc12345-abcd-1234-abcd-123456789abc"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "postmark", "api_key")
end
test "accepts uppercase UUID" do
key = "ABC12345-ABCD-1234-ABCD-123456789ABC"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "postmark", "api_key")
end
test "rejects non-UUID" do
assert {:error, msg} = KeyValidation.validate_email_key("not-a-uuid", "postmark", "api_key")
assert msg =~ "UUID"
end
end
describe "validate_email_key/3 - Resend" do
test "accepts valid Resend key" do
assert {:ok, "re_abc123xyz"} =
KeyValidation.validate_email_key("re_abc123xyz", "resend", "api_key")
end
test "rejects key without re_ prefix" do
assert {:error, msg} = KeyValidation.validate_email_key("not_resend", "resend", "api_key")
assert msg =~ "re_"
end
end
describe "validate_email_key/3 - Mailgun" do
test "accepts classic key- format" do
key = "key-abcdef1234567890abcdef1234567890"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailgun", "api_key")
end
test "accepts newer long key without prefix" do
key = "abcdef1234567890abcdef1234567890abcdef1234"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailgun", "api_key")
end
test "rejects short key without prefix" do
assert {:error, msg} = KeyValidation.validate_email_key("short", "mailgun", "api_key")
assert msg =~ "key-"
end
end
describe "validate_email_key/3 - Brevo" do describe "validate_email_key/3 - Brevo" do
test "accepts valid Brevo key" do test "accepts valid Brevo key" do
key = "xkeysib-" <> String.duplicate("ab", 32) key = "xkeysib-" <> String.duplicate("ab", 32)
@ -211,19 +165,6 @@ defmodule Berrypod.KeyValidationTest do
end end
end end
describe "validate_email_key/3 - MailPace" do
test "accepts valid token" do
key = "abc123def456ghi789"
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailpace", "api_key")
end
test "rejects short token" do
assert {:error, msg} = KeyValidation.validate_email_key("short", "mailpace", "api_key")
assert msg =~ "too short"
assert msg =~ "MailPace"
end
end
describe "validate_email_key/3 - cross-provider detection" do describe "validate_email_key/3 - cross-provider detection" do
test "detects Brevo key pasted into SendGrid" do test "detects Brevo key pasted into SendGrid" do
key = "xkeysib-abc123def456" key = "xkeysib-abc123def456"
@ -237,25 +178,14 @@ defmodule Berrypod.KeyValidationTest do
assert msg =~ "SendGrid key" assert msg =~ "SendGrid key"
end end
test "detects Resend key pasted into Postmark" do test "detects MailerSend key pasted into Brevo" do
key = "re_abc123xyz456"
assert {:error, msg} = KeyValidation.validate_email_key(key, "postmark", "api_key")
assert msg =~ "Resend key"
end
test "detects MailerSend key pasted into Mailgun" do
key = "mlsn.abc123" key = "mlsn.abc123"
assert {:error, msg} = KeyValidation.validate_email_key(key, "mailgun", "api_key") assert {:error, msg} = KeyValidation.validate_email_key(key, "brevo", "api_key")
assert msg =~ "MailerSend key" assert msg =~ "MailerSend key"
end end
end end
describe "validate_email_key/3 - non-api_key fields" do describe "validate_email_key/3 - non-api_key fields" do
test "accepts reasonable value for domain" do
assert {:ok, "mg.example.com"} =
KeyValidation.validate_email_key("mg.example.com", "mailgun", "domain")
end
test "accepts reasonable value for relay" do test "accepts reasonable value for relay" do
assert {:ok, "smtp.example.com"} = assert {:ok, "smtp.example.com"} =
KeyValidation.validate_email_key("smtp.example.com", "smtp", "relay") KeyValidation.validate_email_key("smtp.example.com", "smtp", "relay")

View File

@ -7,7 +7,7 @@ defmodule Berrypod.Mailer.AdaptersTest do
test "returns a list of adapters" do test "returns a list of adapters" do
adapters = Adapters.all() adapters = Adapters.all()
assert is_list(adapters) assert is_list(adapters)
assert length(adapters) >= 9 assert length(adapters) >= 5
end end
test "each adapter has required keys" do test "each adapter has required keys" do
@ -43,10 +43,10 @@ defmodule Berrypod.Mailer.AdaptersTest do
describe "get/1" do describe "get/1" do
test "returns adapter by key" do test "returns adapter by key" do
assert %{key: "smtp", name: "SMTP"} = Adapters.get("smtp") assert %{key: "smtp", name: "SMTP"} = Adapters.get("smtp")
assert %{key: "postmark", name: "Postmark"} = Adapters.get("postmark")
assert %{key: "mailjet", name: "Mailjet"} = Adapters.get("mailjet") assert %{key: "mailjet", name: "Mailjet"} = Adapters.get("mailjet")
assert %{key: "mailpace", name: "MailPace"} = Adapters.get("mailpace")
assert %{key: "postal", name: "Postal"} = Adapters.get("postal") assert %{key: "postal", name: "Postal"} = Adapters.get("postal")
assert is_nil(Adapters.get("postmark"))
assert is_nil(Adapters.get("mailpace"))
end end
test "returns nil for unknown key" do test "returns nil for unknown key" do
@ -70,7 +70,6 @@ defmodule Berrypod.Mailer.AdaptersTest do
test "returns unique keys from all adapters" do test "returns unique keys from all adapters" do
keys = Adapters.all_field_keys() keys = Adapters.all_field_keys()
assert is_list(keys) assert is_list(keys)
assert "email_postmark_api_key" in keys
assert "email_smtp_relay" in keys assert "email_smtp_relay" in keys
assert length(keys) == length(Enum.uniq(keys)) assert length(keys) == length(Enum.uniq(keys))
end end
@ -78,7 +77,7 @@ defmodule Berrypod.Mailer.AdaptersTest do
describe "settings_key/2" do describe "settings_key/2" do
test "namespaces key with adapter" do test "namespaces key with adapter" do
assert Adapters.settings_key("postmark", "api_key") == "email_postmark_api_key" assert Adapters.settings_key("brevo", "api_key") == "email_brevo_api_key"
assert Adapters.settings_key("smtp", "relay") == "email_smtp_relay" assert Adapters.settings_key("smtp", "relay") == "email_smtp_relay"
end end
end end

View File

@ -25,14 +25,14 @@ defmodule Berrypod.MailerTest do
describe "load_config/0" do describe "load_config/0" do
test "loads adapter config from settings" do test "loads adapter config from settings" do
Settings.put_setting("email_adapter", "postmark") Settings.put_setting("email_adapter", "brevo")
Settings.put_secret("email_postmark_api_key", "pm_test_key_123") Settings.put_secret("email_brevo_api_key", "xkeysib-test-key-123")
Mailer.load_config() Mailer.load_config()
config = Application.get_env(:berrypod, Mailer) config = Application.get_env(:berrypod, Mailer)
assert config[:adapter] == Swoosh.Adapters.Postmark assert config[:adapter] == Swoosh.Adapters.Brevo
assert config[:api_key] == "pm_test_key_123" assert config[:api_key] == "xkeysib-test-key-123"
end end
test "loads SMTP config with multiple fields" do test "loads SMTP config with multiple fields" do
@ -283,13 +283,13 @@ defmodule Berrypod.MailerTest do
end end
test "returns adapter key and config when configured from settings" do test "returns adapter key and config when configured from settings" do
Settings.put_setting("email_adapter", "postmark") Settings.put_setting("email_adapter", "brevo")
Settings.put_secret("email_postmark_api_key", "pm_test_key_123") Settings.put_secret("email_brevo_api_key", "xkeysib-test-key-123")
Mailer.load_config() Mailer.load_config()
{adapter_key, config} = Mailer.current_config() {adapter_key, config} = Mailer.current_config()
assert adapter_key == "postmark" assert adapter_key == "brevo"
assert config["api_key"] =~ "•••" assert config["api_key"] =~ "•••"
end end
end end

View File

@ -24,8 +24,7 @@ defmodule BerrypodWeb.EmailSettingsControllerTest do
post(conn, ~p"/admin/settings/email", %{ post(conn, ~p"/admin/settings/email", %{
email: %{ email: %{
adapter: "brevo", adapter: "brevo",
configured_adapter: "brevo", brevo: %{api_key: "xkeysib-abc123def456"}
api_key: "xkeysib-abc123def456"
} }
}) })
@ -34,19 +33,10 @@ defmodule BerrypodWeb.EmailSettingsControllerTest do
assert Settings.get_setting("email_adapter") == "brevo" assert Settings.get_setting("email_adapter") == "brevo"
end end
test "redirects with adapter param when adapter changed", %{conn: conn} do
conn =
post(conn, ~p"/admin/settings/email", %{
email: %{adapter: "resend", configured_adapter: "brevo", api_key: ""}
})
assert redirected_to(conn) == ~p"/admin/settings/email?adapter=resend"
end
test "redirects with error on validation failure", %{conn: conn} do test "redirects with error on validation failure", %{conn: conn} do
conn = conn =
post(conn, ~p"/admin/settings/email", %{ post(conn, ~p"/admin/settings/email", %{
email: %{adapter: "brevo", configured_adapter: "brevo", api_key: ""} email: %{adapter: "brevo", brevo: %{api_key: ""}}
}) })
assert redirected_to(conn) =~ ~p"/admin/settings/email" assert redirected_to(conn) =~ ~p"/admin/settings/email"
@ -56,8 +46,8 @@ defmodule BerrypodWeb.EmailSettingsControllerTest do
describe "POST /admin/settings/email/test" do describe "POST /admin/settings/email/test" do
test "sends test email and redirects", %{conn: conn} do test "sends test email and redirects", %{conn: conn} do
Settings.put_setting("email_adapter", "postmark") Settings.put_setting("email_adapter", "brevo")
Settings.put_secret("email_postmark_api_key", "pm_test_abc") Settings.put_secret("email_brevo_api_key", "xkeysib-test-abc")
conn = post(conn, ~p"/admin/settings/email/test") conn = post(conn, ~p"/admin/settings/email/test")

View File

@ -33,27 +33,17 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
# Provider names rendered as radio cards # Provider names rendered as radio cards
assert html =~ "Brevo" assert html =~ "Brevo"
assert html =~ "Mailjet" assert html =~ "Mailjet"
assert html =~ "MailPace"
assert html =~ "Postal" assert html =~ "Postal"
end end
test "shows all three provider groups", %{conn: conn} do test "shows providers and advanced section", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings/email") {:ok, _view, html} = live(conn, ~p"/admin/settings/email")
# All-email providers
assert html =~ "Popular providers"
assert html =~ "Brevo" assert html =~ "Brevo"
assert html =~ "SendGrid" assert html =~ "SendGrid"
assert html =~ "Mailjet" assert html =~ "Mailjet"
assert html =~ "MailerSend" assert html =~ "MailerSend"
# Transactional providers
assert html =~ "Transactional only"
assert html =~ "Resend"
assert html =~ "Postmark"
assert html =~ "Mailgun"
assert html =~ "MailPace"
# Advanced in details # Advanced in details
assert html =~ "Already have your own email server?" assert html =~ "Already have your own email server?"
assert html =~ "SMTP" assert html =~ "SMTP"
@ -64,10 +54,20 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
{:ok, _view, html} = live(conn, ~p"/admin/settings/email") {:ok, _view, html} = live(conn, ~p"/admin/settings/email")
assert html =~ "needs an email provider" assert html =~ "needs an email provider"
assert html =~ "300 emails/day free"
end end
test "selecting a provider shows its config fields", %{conn: conn} do test "renders all adapter field sections in the form", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
# All adapters have their config sections rendered (CSS controls visibility)
assert html =~ ~s(data-adapter="brevo")
assert html =~ ~s(data-adapter="sendgrid")
assert html =~ ~s(data-adapter="mailjet")
assert html =~ ~s(data-adapter="smtp")
assert html =~ ~s(data-adapter="postal")
end
test "selecting a provider updates via phx-change", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings/email") {:ok, view, _html} = live(conn, ~p"/admin/settings/email")
# Select SMTP via form change (radio inputs fire phx-change) # Select SMTP via form change (radio inputs fire phx-change)
@ -76,35 +76,9 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "smtp"}}) |> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "smtp"}})
|> render_change() |> render_change()
# SMTP fields are always rendered; check the step title changes
assert html =~ "Server host" assert html =~ "Server host"
assert html =~ "Port" assert html =~ "Port"
assert html =~ "Username"
assert html =~ "Password"
end
test "selecting a different provider shows different fields", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
# Select Brevo which needs just an api_key
html =
view
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}})
|> render_change()
assert html =~ "API key"
assert html =~ "Brevo"
end
test "selecting a transactional provider shows its config", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
html =
view
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "resend"}})
|> render_change()
assert html =~ "API key"
assert html =~ "Resend"
end end
test "saving config persists settings", %{conn: conn} do test "saving config persists settings", %{conn: conn} do
@ -115,11 +89,11 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}}) |> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}})
|> render_change() |> render_change()
# Submit with an API key # Submit with namespaced fields: email[brevo][api_key]
html = html =
view view
|> form("form[phx-submit=\"save\"]", %{ |> form("form[phx-submit=\"save\"]", %{
email: %{adapter: "brevo", api_key: "xkeysib-abc123def456"} email: %{adapter: "brevo", brevo: %{api_key: "xkeysib-abc123def456"}}
}) })
|> render_submit() |> render_submit()
@ -138,7 +112,9 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
# Submit without API key # Submit without API key
html = html =
view view
|> form("form[phx-submit=\"save\"]", %{email: %{adapter: "brevo", api_key: ""}}) |> form("form[phx-submit=\"save\"]", %{
email: %{adapter: "brevo", brevo: %{api_key: ""}}
})
|> render_submit() |> render_submit()
assert html =~ "API key is required" assert html =~ "API key is required"
@ -154,7 +130,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
html = html =
view view
|> form("form[phx-submit=\"save\"]", %{ |> form("form[phx-submit=\"save\"]", %{
email: %{adapter: "smtp", relay: "smtp.example.com", port: "abc"} email: %{adapter: "smtp", smtp: %{relay: "smtp.example.com", port: "abc"}}
}) })
|> render_submit() |> render_submit()
@ -162,8 +138,8 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
end end
test "shows test email section when configured", %{conn: conn} do test "shows test email section when configured", %{conn: conn} do
Settings.put_setting("email_adapter", "postmark") Settings.put_setting("email_adapter", "brevo")
Settings.put_secret("email_postmark_api_key", "pm_test_abc") Settings.put_secret("email_brevo_api_key", "xkeysib-test-abc")
{:ok, _view, html} = live(conn, ~p"/admin/settings/email") {:ok, _view, html} = live(conn, ~p"/admin/settings/email")
@ -182,8 +158,8 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
end end
test "sending test email shows success and sets verified flag", %{conn: conn} do test "sending test email shows success and sets verified flag", %{conn: conn} do
Settings.put_setting("email_adapter", "postmark") Settings.put_setting("email_adapter", "brevo")
Settings.put_secret("email_postmark_api_key", "pm_test_abc") Settings.put_secret("email_brevo_api_key", "xkeysib-test-abc")
{:ok, view, _html} = live(conn, ~p"/admin/settings/email") {:ok, view, _html} = live(conn, ~p"/admin/settings/email")
@ -205,14 +181,14 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
{:ok, view, _html} = live(conn, ~p"/admin/settings/email") {:ok, view, _html} = live(conn, ~p"/admin/settings/email")
# Switch to Brevo and save # Switch to Brevo and save (namespaced fields)
view view
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}}) |> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}})
|> render_change() |> render_change()
view view
|> form("form[phx-submit=\"save\"]", %{ |> form("form[phx-submit=\"save\"]", %{
email: %{adapter: "brevo", api_key: "xkeysib-switch-test"} email: %{adapter: "brevo", brevo: %{api_key: "xkeysib-switch-test"}}
}) })
|> render_submit() |> render_submit()
@ -236,7 +212,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
view view
|> form("form[phx-submit=\"save\"]", %{ |> form("form[phx-submit=\"save\"]", %{
email: %{adapter: "brevo", api_key: "xkeysib-def789ghi012"} email: %{adapter: "brevo", brevo: %{api_key: "xkeysib-def789ghi012"}}
}) })
|> render_submit() |> render_submit()
@ -262,13 +238,6 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
assert html =~ "API key" assert html =~ "API key"
end end
test "adapter query param preselects provider", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings/email?adapter=resend")
assert html =~ "Resend"
assert html =~ "API key"
end
test "from_checklist param shows checklist banner", %{conn: conn} do test "from_checklist param shows checklist banner", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/settings/email?from=checklist") {:ok, _view, html} = live(conn, ~p"/admin/settings/email?from=checklist")