add contextual prompts for skipped setup steps
All checks were successful
deploy / deploy (push) Successful in 1m26s
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:
parent
005ebca432
commit
67a26eb6b4
@ -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)
|
- 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)
|
||||||
- Deployed on Fly.io with observability (LiveDashboard, ErrorTracker, structured logging)
|
- 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
|
## 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 |
|
| 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 | 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 |
|
| 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 |
|
| 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 |
|
||||||
| H | External links UX (new tabs, icons, aria labels) | 1h | done |
|
| H | External links UX (new tabs, icons, aria labels) | 1h | done |
|
||||||
|
|||||||
@ -1022,6 +1022,9 @@
|
|||||||
max-width: 80rem;
|
max-width: 80rem;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Shop container (body-level defaults) ── */
|
/* ── Shop container (body-level defaults) ── */
|
||||||
@ -1528,6 +1531,17 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--t-font-body);
|
font-family: var(--t-font-body);
|
||||||
margin-bottom: 0.5rem;
|
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 ── */
|
/* ── Cart item row ── */
|
||||||
@ -1864,6 +1878,13 @@
|
|||||||
text-align: center;
|
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 ── */
|
||||||
|
|
||||||
.content-body {
|
.content-body {
|
||||||
|
|||||||
@ -284,6 +284,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .themed-button-outline {
|
& .themed-button-outline {
|
||||||
|
|||||||
@ -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 |
|
| 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 | 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 |
|
| 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 |
|
||||||
| H | External links UX (new tabs, icons, aria) | 1h | planned |
|
| H | External links UX (new tabs, icons, aria) | 1h | planned |
|
||||||
|
|||||||
@ -19,8 +19,7 @@ defmodule BerrypodWeb.CartHook do
|
|||||||
import Phoenix.Component, only: [assign: 3]
|
import Phoenix.Component, only: [assign: 3]
|
||||||
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
|
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
|
||||||
|
|
||||||
alias Berrypod.Cart
|
alias Berrypod.{Cart, Settings, Shipping}
|
||||||
alias Berrypod.Shipping
|
|
||||||
|
|
||||||
def on_mount(:mount_cart, _params, session, socket) do
|
def on_mount(:mount_cart, _params, session, socket) do
|
||||||
cart_items = Cart.get_from_session(session)
|
cart_items = Cart.get_from_session(session)
|
||||||
@ -34,6 +33,7 @@ defmodule BerrypodWeb.CartHook do
|
|||||||
|> update_cart_assigns(cart_items)
|
|> update_cart_assigns(cart_items)
|
||||||
|> assign(:cart_drawer_open, false)
|
|> assign(:cart_drawer_open, false)
|
||||||
|> assign(:cart_status, nil)
|
|> 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_events, :handle_event, &handle_cart_event/3)
|
||||||
|> attach_hook(:cart_info, :handle_info, &handle_cart_info/2)
|
|> attach_hook(:cart_info, :handle_info, &handle_cart_info/2)
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
|||||||
attr :shipping_estimate, :integer, default: nil
|
attr :shipping_estimate, :integer, default: nil
|
||||||
attr :country_code, :string, default: "GB"
|
attr :country_code, :string, default: "GB"
|
||||||
attr :available_countries, :list, default: []
|
attr :available_countries, :list, default: []
|
||||||
|
attr :stripe_connected, :boolean, default: true
|
||||||
|
|
||||||
def cart_drawer(assigns) do
|
def cart_drawer(assigns) do
|
||||||
assigns =
|
assigns =
|
||||||
@ -131,20 +132,20 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
|||||||
<span>{if @shipping_estimate, do: "Estimated total", else: "Subtotal"}</span>
|
<span>{if @shipping_estimate, do: "Estimated total", else: "Subtotal"}</span>
|
||||||
<span>{@display_total}</span>
|
<span>{@display_total}</span>
|
||||||
</div>
|
</div>
|
||||||
<%= if @mode == :preview do %>
|
<%= cond do %>
|
||||||
<button
|
<% @mode == :preview -> %>
|
||||||
type="button"
|
<button type="button" class="cart-drawer-checkout">
|
||||||
class="cart-drawer-checkout"
|
|
||||||
>
|
|
||||||
Checkout
|
Checkout
|
||||||
</button>
|
</button>
|
||||||
<% else %>
|
<% !@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">
|
<form action="/checkout" method="post">
|
||||||
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
|
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
|
||||||
<button
|
<button type="submit" class="cart-drawer-checkout">
|
||||||
type="submit"
|
|
||||||
class="cart-drawer-checkout"
|
|
||||||
>
|
|
||||||
Checkout
|
Checkout
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -451,6 +452,7 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
|||||||
attr :country_code, :string, default: "GB"
|
attr :country_code, :string, default: "GB"
|
||||||
attr :available_countries, :list, default: []
|
attr :available_countries, :list, default: []
|
||||||
attr :mode, :atom, default: :live
|
attr :mode, :atom, default: :live
|
||||||
|
attr :stripe_connected, :boolean, default: true
|
||||||
|
|
||||||
def order_summary(assigns) do
|
def order_summary(assigns) do
|
||||||
assigns =
|
assigns =
|
||||||
@ -487,7 +489,8 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if @mode == :preview do %>
|
<%= cond do %>
|
||||||
|
<% @mode == :preview -> %>
|
||||||
<.shop_button class="order-summary-checkout">
|
<.shop_button class="order-summary-checkout">
|
||||||
Checkout
|
Checkout
|
||||||
</.shop_button>
|
</.shop_button>
|
||||||
@ -498,7 +501,18 @@ defmodule BerrypodWeb.ShopComponents.Cart do
|
|||||||
>
|
>
|
||||||
Continue shopping
|
Continue shopping
|
||||||
</.shop_button_outline>
|
</.shop_button_outline>
|
||||||
<% else %>
|
<% !@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">
|
<form action="/checkout" method="post" class="order-summary-checkout-form">
|
||||||
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
|
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
|
||||||
<.shop_button type="submit" class="order-summary-checkout">
|
<.shop_button type="submit" class="order-summary-checkout">
|
||||||
|
|||||||
@ -52,7 +52,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
cart_subtotal cart_total cart_drawer_open cart_status active_page error_page is_admin
|
cart_subtotal cart_total cart_drawer_open cart_status active_page error_page is_admin
|
||||||
search_query search_results search_open categories shipping_estimate
|
search_query search_results search_open categories shipping_estimate
|
||||||
country_code available_countries editing editor_current_path editor_sidebar_open
|
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 """
|
@doc """
|
||||||
Extracts the assigns relevant to `shop_layout` from a full assigns map.
|
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 :footer_nav_items, :list, default: []
|
||||||
attr :newsletter_enabled, :boolean, default: false
|
attr :newsletter_enabled, :boolean, default: false
|
||||||
attr :newsletter_state, :atom, default: :idle
|
attr :newsletter_state, :atom, default: :idle
|
||||||
|
attr :stripe_connected, :boolean, default: true
|
||||||
|
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
@ -156,6 +157,7 @@ defmodule BerrypodWeb.ShopComponents.Layout do
|
|||||||
shipping_estimate={@shipping_estimate}
|
shipping_estimate={@shipping_estimate}
|
||||||
country_code={@country_code}
|
country_code={@country_code}
|
||||||
available_countries={@available_countries}
|
available_countries={@available_countries}
|
||||||
|
stripe_connected={@stripe_connected}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<.search_modal
|
<.search_modal
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
defmodule BerrypodWeb.CheckoutController do
|
defmodule BerrypodWeb.CheckoutController do
|
||||||
use BerrypodWeb, :controller
|
use BerrypodWeb, :controller
|
||||||
|
|
||||||
alias Berrypod.{Analytics, Cart}
|
alias Berrypod.{Analytics, Cart, Settings}
|
||||||
alias Berrypod.Orders
|
alias Berrypod.Orders
|
||||||
alias Berrypod.Shipping
|
alias Berrypod.Shipping
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
def create(conn, _params) do
|
def create(conn, _params) do
|
||||||
|
unless Settings.has_secret?("stripe_api_key") do
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Checkout isn't available yet")
|
||||||
|
|> redirect(to: ~p"/cart")
|
||||||
|
else
|
||||||
cart_items = Cart.get_from_session(get_session(conn))
|
cart_items = Cart.get_from_session(get_session(conn))
|
||||||
hydrated = Cart.hydrate(cart_items)
|
hydrated = Cart.hydrate(cart_items)
|
||||||
|
|
||||||
@ -20,6 +25,7 @@ defmodule BerrypodWeb.CheckoutController do
|
|||||||
create_checkout(conn, hydrated)
|
create_checkout(conn, hydrated)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp create_checkout(conn, hydrated_items) do
|
defp create_checkout(conn, hydrated_items) do
|
||||||
# Create a pending order with price snapshots
|
# Create a pending order with price snapshots
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
defmodule BerrypodWeb.Admin.OrderShow do
|
defmodule BerrypodWeb.Admin.OrderShow do
|
||||||
use BerrypodWeb, :live_view
|
use BerrypodWeb, :live_view
|
||||||
|
|
||||||
alias Berrypod.{ActivityLog, Orders}
|
alias Berrypod.{ActivityLog, Mailer, Orders}
|
||||||
alias Berrypod.Cart
|
alias Berrypod.Cart
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -25,6 +25,7 @@ defmodule BerrypodWeb.Admin.OrderShow do
|
|||||||
|> assign(:page_title, order.order_number)
|
|> assign(:page_title, order.order_number)
|
||||||
|> assign(:order, order)
|
|> assign(:order, order)
|
||||||
|> assign(:timeline, timeline)
|
|> assign(:timeline, timeline)
|
||||||
|
|> assign(:email_configured, Mailer.email_configured?())
|
||||||
|
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
@ -48,6 +49,25 @@ defmodule BerrypodWeb.Admin.OrderShow do
|
|||||||
</div>
|
</div>
|
||||||
</.header>
|
</.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 →
|
||||||
|
</.link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="admin-grid order-detail-grid">
|
<div class="admin-grid order-detail-grid">
|
||||||
<%!-- order info --%>
|
<%!-- order info --%>
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
|
|||||||
@ -603,6 +603,7 @@ defmodule BerrypodWeb.PageRenderer do
|
|||||||
shipping_estimate={assigns[:shipping_estimate]}
|
shipping_estimate={assigns[:shipping_estimate]}
|
||||||
country_code={assigns[:country_code] || "GB"}
|
country_code={assigns[:country_code] || "GB"}
|
||||||
available_countries={assigns[:available_countries] || []}
|
available_countries={assigns[:available_countries] || []}
|
||||||
|
stripe_connected={assigns[:stripe_connected] || false}
|
||||||
mode={@mode}
|
mode={@mode}
|
||||||
/>
|
/>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user