add contextual prompts for skipped setup steps
All checks were successful
deploy / deploy (push) Successful in 1m26s

Disable checkout when Stripe isn't connected (cart drawer, cart page,
and early guard in checkout controller to prevent orphaned orders).
Show amber warning on order detail when email isn't configured.
Fix pre-existing missing vertical spacing between page blocks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-04 14:02:49 +00:00
parent 005ebca432
commit 67a26eb6b4
10 changed files with 122 additions and 53 deletions

View File

@ -29,7 +29,7 @@ Tier 1 MVP complete. Tier 2 production readiness complete (except Litestream and
- Fully Tailwind-free CSS (12 KB gzipped shop+theme, 95 KB gzipped admin total)
- CI pipeline (compile warnings, format, credo, dialyzer, tests)
- Deployed on Fly.io with observability (LiveDashboard, ErrorTracker, structured logging)
- 1679+ tests passing, 99-100 PageSpeed mobile
- 1716+ tests passing, 99-100 PageSpeed mobile
## Next up
@ -59,9 +59,9 @@ 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 |
| 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 | planned |
| 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 |
| E | Contextual prompts for skipped steps (products, checkout, order detail) | 2h | planned |
| E | Contextual prompts for skipped steps (products, checkout, order detail) | 2h | done |
| F | Dashboard checklist and messaging rework | 2h | planned |
| G | Coming soon page fixes (logo layout, admin login link) | 30m | done |
| H | External links UX (new tabs, icons, aria labels) | 1h | done |

View File

@ -1022,6 +1022,9 @@
max-width: 80rem;
margin-inline: auto;
padding: 2rem 1rem;
display: flex;
flex-direction: column;
gap: 2rem;
}
/* ── Shop container (body-level defaults) ── */
@ -1528,6 +1531,17 @@
cursor: pointer;
font-family: var(--t-font-body);
margin-bottom: 0.5rem;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.cart-drawer-notice {
font-size: var(--t-text-sm);
color: var(--t-text-secondary);
text-align: center;
}
/* ── Cart item row ── */
@ -1864,6 +1878,13 @@
text-align: center;
}
.order-summary-notice {
font-size: var(--t-text-sm);
color: var(--t-text-secondary);
text-align: center;
margin-bottom: 0.75rem;
}
/* ── Content body ── */
.content-body {

View File

@ -284,6 +284,11 @@
cursor: pointer;
padding: 0.75rem 1.5rem;
font-weight: 600;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
& .themed-button-outline {

View File

@ -149,7 +149,7 @@ Increase input field border contrast to meet WCAG AA (3:1 minimum for UI compone
| B | Guided setup flow with progress bar | 4h | planned |
| C | Forgiving API key validation | 1.5h | planned |
| D | Email provider setup UX rework | 2h | planned |
| E | Contextual prompts for skipped steps | 2h | planned |
| E | Contextual prompts for skipped steps | 2h | done |
| F | Dashboard checklist and messaging rework | 2h | planned |
| G | Coming soon page fixes (logo + admin link) | 30m | planned |
| H | External links UX (new tabs, icons, aria) | 1h | planned |

View File

@ -19,8 +19,7 @@ defmodule BerrypodWeb.CartHook do
import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
alias Berrypod.Cart
alias Berrypod.Shipping
alias Berrypod.{Cart, Settings, Shipping}
def on_mount(:mount_cart, _params, session, socket) do
cart_items = Cart.get_from_session(session)
@ -34,6 +33,7 @@ defmodule BerrypodWeb.CartHook do
|> update_cart_assigns(cart_items)
|> assign(:cart_drawer_open, false)
|> assign(:cart_status, nil)
|> assign(:stripe_connected, Settings.has_secret?("stripe_api_key"))
|> attach_hook(:cart_events, :handle_event, &handle_cart_event/3)
|> attach_hook(:cart_info, :handle_info, &handle_cart_info/2)

View File

@ -42,6 +42,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
attr :shipping_estimate, :integer, default: nil
attr :country_code, :string, default: "GB"
attr :available_countries, :list, default: []
attr :stripe_connected, :boolean, default: true
def cart_drawer(assigns) do
assigns =
@ -131,23 +132,23 @@ defmodule BerrypodWeb.ShopComponents.Cart do
<span>{if @shipping_estimate, do: "Estimated total", else: "Subtotal"}</span>
<span>{@display_total}</span>
</div>
<%= if @mode == :preview do %>
<button
type="button"
class="cart-drawer-checkout"
>
Checkout
</button>
<% else %>
<form action="/checkout" method="post">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<button
type="submit"
class="cart-drawer-checkout"
>
<%= cond do %>
<% @mode == :preview -> %>
<button type="button" class="cart-drawer-checkout">
Checkout
</button>
</form>
<% !@stripe_connected -> %>
<button type="button" disabled class="cart-drawer-checkout">
Checkout
</button>
<p class="cart-drawer-notice">Checkout isn't available yet.</p>
<% true -> %>
<form action="/checkout" method="post">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<button type="submit" class="cart-drawer-checkout">
Checkout
</button>
</form>
<% end %>
</div>
</div>
@ -451,6 +452,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
attr :country_code, :string, default: "GB"
attr :available_countries, :list, default: []
attr :mode, :atom, default: :live
attr :stripe_connected, :boolean, default: true
def order_summary(assigns) do
assigns =
@ -487,30 +489,42 @@ defmodule BerrypodWeb.ShopComponents.Cart do
</div>
</div>
<%= if @mode == :preview do %>
<.shop_button class="order-summary-checkout">
Checkout
</.shop_button>
<.shop_button_outline
phx-click="change_preview_page"
phx-value-page="collection"
class="order-summary-continue"
>
Continue shopping
</.shop_button_outline>
<% else %>
<form action="/checkout" method="post" class="order-summary-checkout-form">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<.shop_button type="submit" class="order-summary-checkout">
<%= cond do %>
<% @mode == :preview -> %>
<.shop_button class="order-summary-checkout">
Checkout
</.shop_button>
</form>
<.shop_link_outline
href="/collections/all"
class="order-summary-continue"
>
Continue shopping
</.shop_link_outline>
<.shop_button_outline
phx-click="change_preview_page"
phx-value-page="collection"
class="order-summary-continue"
>
Continue shopping
</.shop_button_outline>
<% !@stripe_connected -> %>
<.shop_button disabled class="order-summary-checkout">
Checkout
</.shop_button>
<p class="order-summary-notice">Checkout isn't available yet.</p>
<.shop_link_outline
href="/collections/all"
class="order-summary-continue"
>
Continue shopping
</.shop_link_outline>
<% true -> %>
<form action="/checkout" method="post" class="order-summary-checkout-form">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<.shop_button type="submit" class="order-summary-checkout">
Checkout
</.shop_button>
</form>
<.shop_link_outline
href="/collections/all"
class="order-summary-continue"
>
Continue shopping
</.shop_link_outline>
<% end %>
</.shop_card>
"""

View File

@ -52,7 +52,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
cart_subtotal cart_total cart_drawer_open cart_status active_page error_page is_admin
search_query search_results search_open categories shipping_estimate
country_code available_countries editing editor_current_path editor_sidebar_open
header_nav_items footer_nav_items newsletter_enabled newsletter_state)a
header_nav_items footer_nav_items newsletter_enabled newsletter_state stripe_connected)a
@doc """
Extracts the assigns relevant to `shop_layout` from a full assigns map.
@ -101,6 +101,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
attr :footer_nav_items, :list, default: []
attr :newsletter_enabled, :boolean, default: false
attr :newsletter_state, :atom, default: :idle
attr :stripe_connected, :boolean, default: true
slot :inner_block, required: true
@ -156,6 +157,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
shipping_estimate={@shipping_estimate}
country_code={@country_code}
available_countries={@available_countries}
stripe_connected={@stripe_connected}
/>
<.search_modal

View File

@ -1,23 +1,29 @@
defmodule BerrypodWeb.CheckoutController do
use BerrypodWeb, :controller
alias Berrypod.{Analytics, Cart}
alias Berrypod.{Analytics, Cart, Settings}
alias Berrypod.Orders
alias Berrypod.Shipping
require Logger
def create(conn, _params) do
cart_items = Cart.get_from_session(get_session(conn))
hydrated = Cart.hydrate(cart_items)
if hydrated == [] do
unless Settings.has_secret?("stripe_api_key") do
conn
|> put_flash(:error, "Your basket is empty")
|> put_flash(:error, "Checkout isn't available yet")
|> redirect(to: ~p"/cart")
else
track_checkout_start(conn)
create_checkout(conn, hydrated)
cart_items = Cart.get_from_session(get_session(conn))
hydrated = Cart.hydrate(cart_items)
if hydrated == [] do
conn
|> put_flash(:error, "Your basket is empty")
|> redirect(to: ~p"/cart")
else
track_checkout_start(conn)
create_checkout(conn, hydrated)
end
end
end

View File

@ -1,7 +1,7 @@
defmodule BerrypodWeb.Admin.OrderShow do
use BerrypodWeb, :live_view
alias Berrypod.{ActivityLog, Orders}
alias Berrypod.{ActivityLog, Mailer, Orders}
alias Berrypod.Cart
@impl true
@ -25,6 +25,7 @@ defmodule BerrypodWeb.Admin.OrderShow do
|> assign(:page_title, order.order_number)
|> assign(:order, order)
|> assign(:timeline, timeline)
|> assign(:email_configured, Mailer.email_configured?())
{:ok, socket}
end
@ -48,6 +49,25 @@ defmodule BerrypodWeb.Admin.OrderShow do
</div>
</.header>
<div :if={!@email_configured} class="admin-callout-warning admin-card-spaced">
<div class="admin-callout-warning-body">
<span class="admin-callout-warning-icon">
<.icon name="hero-exclamation-triangle" class="size-5" />
</span>
<div>
<p class="admin-callout-warning-title">
Order confirmation emails aren't being sent
</p>
<p class="admin-callout-warning-desc">
Set up an email provider to send order confirmations and shipping updates automatically.
<.link navigate={~p"/admin/settings/email"} class="admin-link">
Set up email &rarr;
</.link>
</p>
</div>
</div>
</div>
<div class="admin-grid order-detail-grid">
<%!-- order info --%>
<div class="admin-card">

View File

@ -603,6 +603,7 @@ defmodule BerrypodWeb.PageRenderer do
shipping_estimate={assigns[:shipping_estimate]}
country_code={assigns[:country_code] || "GB"}
available_countries={assigns[:available_countries] || []}
stripe_connected={assigns[:stripe_connected] || false}
mode={@mode}
/>
<% end %>