From 67a26eb6b499ad8f346003a25589df13e4f6c03a Mon Sep 17 00:00:00 2001 From: jamey Date: Wed, 4 Mar 2026 14:02:49 +0000 Subject: [PATCH] add contextual prompts for skipped setup steps 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 --- PROGRESS.md | 6 +- assets/css/shop/components.css | 21 +++++ assets/css/theme-layer2-attributes.css | 5 ++ docs/plans/onboarding-ux.md | 2 +- lib/berrypod_web/cart_hook.ex | 4 +- .../components/shop_components/cart.ex | 88 +++++++++++-------- .../components/shop_components/layout.ex | 4 +- .../controllers/checkout_controller.ex | 22 +++-- lib/berrypod_web/live/admin/order_show.ex | 22 ++++- lib/berrypod_web/page_renderer.ex | 1 + 10 files changed, 122 insertions(+), 53 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 6d77f15..27c077f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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 | diff --git a/assets/css/shop/components.css b/assets/css/shop/components.css index 10951c2..85cc4a3 100644 --- a/assets/css/shop/components.css +++ b/assets/css/shop/components.css @@ -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 { diff --git a/assets/css/theme-layer2-attributes.css b/assets/css/theme-layer2-attributes.css index c68fbd2..602f9d5 100644 --- a/assets/css/theme-layer2-attributes.css +++ b/assets/css/theme-layer2-attributes.css @@ -284,6 +284,11 @@ cursor: pointer; padding: 0.75rem 1.5rem; font-weight: 600; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } } & .themed-button-outline { diff --git a/docs/plans/onboarding-ux.md b/docs/plans/onboarding-ux.md index 7a9f9ea..067e58e 100644 --- a/docs/plans/onboarding-ux.md +++ b/docs/plans/onboarding-ux.md @@ -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 | diff --git a/lib/berrypod_web/cart_hook.ex b/lib/berrypod_web/cart_hook.ex index c21463a..817bd9e 100644 --- a/lib/berrypod_web/cart_hook.ex +++ b/lib/berrypod_web/cart_hook.ex @@ -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) diff --git a/lib/berrypod_web/components/shop_components/cart.ex b/lib/berrypod_web/components/shop_components/cart.ex index 0200024..7677560 100644 --- a/lib/berrypod_web/components/shop_components/cart.ex +++ b/lib/berrypod_web/components/shop_components/cart.ex @@ -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 {if @shipping_estimate, do: "Estimated total", else: "Subtotal"} {@display_total} - <%= if @mode == :preview do %> - - <% else %> -
- - -
+ <% !@stripe_connected -> %> + +

Checkout isn't available yet.

+ <% true -> %> +
+ + +
<% end %> @@ -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 - <%= if @mode == :preview do %> - <.shop_button class="order-summary-checkout"> - Checkout - - <.shop_button_outline - phx-click="change_preview_page" - phx-value-page="collection" - class="order-summary-continue" - > - Continue shopping - - <% else %> -
- - <.shop_button type="submit" class="order-summary-checkout"> + <%= cond do %> + <% @mode == :preview -> %> + <.shop_button class="order-summary-checkout"> Checkout -
- <.shop_link_outline - href="/collections/all" - class="order-summary-continue" - > - Continue shopping - + <.shop_button_outline + phx-click="change_preview_page" + phx-value-page="collection" + class="order-summary-continue" + > + Continue shopping + + <% !@stripe_connected -> %> + <.shop_button disabled class="order-summary-checkout"> + Checkout + +

Checkout isn't available yet.

+ <.shop_link_outline + href="/collections/all" + class="order-summary-continue" + > + Continue shopping + + <% true -> %> +
+ + <.shop_button type="submit" class="order-summary-checkout"> + Checkout + +
+ <.shop_link_outline + href="/collections/all" + class="order-summary-continue" + > + Continue shopping + <% end %> """ diff --git a/lib/berrypod_web/components/shop_components/layout.ex b/lib/berrypod_web/components/shop_components/layout.ex index 23516c7..1450b9a 100644 --- a/lib/berrypod_web/components/shop_components/layout.ex +++ b/lib/berrypod_web/components/shop_components/layout.ex @@ -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 diff --git a/lib/berrypod_web/controllers/checkout_controller.ex b/lib/berrypod_web/controllers/checkout_controller.ex index fe989e6..2b2a74f 100644 --- a/lib/berrypod_web/controllers/checkout_controller.ex +++ b/lib/berrypod_web/controllers/checkout_controller.ex @@ -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 diff --git a/lib/berrypod_web/live/admin/order_show.ex b/lib/berrypod_web/live/admin/order_show.ex index 7a405fd..0ba20aa 100644 --- a/lib/berrypod_web/live/admin/order_show.ex +++ b/lib/berrypod_web/live/admin/order_show.ex @@ -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 +
+
+ + <.icon name="hero-exclamation-triangle" class="size-5" /> + +
+

+ Order confirmation emails aren't being sent +

+

+ 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 → + +

+
+
+
+
<%!-- order info --%>
diff --git a/lib/berrypod_web/page_renderer.ex b/lib/berrypod_web/page_renderer.ex index 9608776..562c604 100644 --- a/lib/berrypod_web/page_renderer.ex +++ b/lib/berrypod_web/page_renderer.ex @@ -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 %>