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 %>