Add SummaryStep, PaymentStep, ConfirmationStep (wizard steps 13-15)

SummaryStep (step 13):
- Accordion sections with edit IconButtons linking back to each step
- dl/dt/dd definition list for label-value pairs
- Total bar with prominent price display (aria-live)
- Share plan icon button, deposit display
- Pre-planning: "Save your plan" CTA; at-need: "Confirm" CTA

PaymentStep (step 14):
- Payment plan (full/deposit) shown before method (amount before how)
- ToggleButtonGroup for plan + method selection
- Card: PayWay iframe slot with placeholder; Bank: account details display
- Terms checkbox with service agreement + privacy links
- Security reassurance (lock icon, no-surprise copy)

ConfirmationStep (step 15):
- Terminal page — no back button, no progress indicator
- At-need: "submitted" + callback timeframe + arranger contact
- Pre-planning: "saved" + return-anytime + family-ready copy
- Muted success icon (not celebratory), next-steps link buttons
- VenueCard selected prop also staged (from step 7 work)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 15:08:41 +11:00
parent 77bac1478f
commit 36757bcdb0
9 changed files with 1182 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { PaymentStep } from './PaymentStep';
import type { PaymentStepValues, PaymentStepErrors } from './PaymentStep';
import { Navigation } from '../../organisms/Navigation';
import Box from '@mui/material/Box';
// ─── Helpers ─────────────────────────────────────────────────────────────────
const FALogo = () => (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/brandlogo/logo-full.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
/>
<Box
component="img"
src="/brandlogo/logo-short.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
/>
</Box>
);
const nav = (
<Navigation
logo={<FALogo />}
items={[
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
]}
/>
);
const defaultValues: PaymentStepValues = {
paymentPlan: null,
paymentMethod: null,
termsAccepted: false,
};
// ─── Meta ────────────────────────────────────────────────────────────────────
const meta: Meta<typeof PaymentStep> = {
title: 'Pages/PaymentStep',
component: PaymentStep,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof PaymentStep>;
// ─── Interactive (default) ──────────────────────────────────────────────────
/** Full interactive payment flow */
export const Default: Story = {
render: () => {
const [values, setValues] = useState<PaymentStepValues>({ ...defaultValues });
const [errors, setErrors] = useState<PaymentStepErrors>({});
const handleConfirm = () => {
const newErrors: PaymentStepErrors = {};
if (!values.paymentPlan) newErrors.paymentPlan = 'Please choose a payment option.';
if (!values.paymentMethod) newErrors.paymentMethod = 'Please choose a payment method.';
if (!values.termsAccepted)
newErrors.termsAccepted = 'Please review and accept the service agreement to continue.';
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) alert('Payment confirmed!');
};
return (
<PaymentStep
values={values}
onChange={(v) => {
setValues(v);
setErrors({});
}}
onConfirmPayment={handleConfirm}
onBack={() => alert('Back to summary')}
errors={errors}
totalPrice={9850}
depositAmount={2000}
navigation={nav}
/>
);
},
};
// ─── Card selected ──────────────────────────────────────────────────────────
/** Card payment selected — shows payment form placeholder */
export const CardPayment: Story = {
render: () => {
const [values, setValues] = useState<PaymentStepValues>({
paymentPlan: 'full',
paymentMethod: 'Card',
termsAccepted: false,
});
return (
<PaymentStep
values={values}
onChange={setValues}
onConfirmPayment={() => alert('Confirm')}
onBack={() => alert('Back')}
totalPrice={9850}
navigation={nav}
/>
);
},
};
// ─── Bank transfer selected ─────────────────────────────────────────────────
/** Bank transfer selected — shows account details */
export const BankTransfer: Story = {
render: () => {
const [values, setValues] = useState<PaymentStepValues>({
paymentPlan: 'deposit',
paymentMethod: 'Bank',
termsAccepted: true,
});
return (
<PaymentStep
values={values}
onChange={setValues}
onConfirmPayment={() => alert('Confirm')}
onBack={() => alert('Back')}
totalPrice={9850}
depositAmount={2000}
navigation={nav}
/>
);
},
};
// ─── Validation errors ──────────────────────────────────────────────────────
/** All errors showing */
export const WithErrors: Story = {
render: () => {
const [values, setValues] = useState<PaymentStepValues>({ ...defaultValues });
return (
<PaymentStep
values={values}
onChange={setValues}
onConfirmPayment={() => {}}
errors={{
paymentPlan: 'Please choose a payment option.',
paymentMethod: 'Please choose a payment method.',
termsAccepted: 'Please review and accept the service agreement to continue.',
}}
totalPrice={9850}
navigation={nav}
/>
);
},
};
// ─── Processing ─────────────────────────────────────────────────────────────
/** Payment processing */
export const Processing: Story = {
render: () => {
const [values, setValues] = useState<PaymentStepValues>({
paymentPlan: 'full',
paymentMethod: 'Card',
termsAccepted: true,
});
return (
<PaymentStep
values={values}
onChange={setValues}
onConfirmPayment={() => {}}
loading
totalPrice={9850}
navigation={nav}
/>
);
},
};

View File

@@ -0,0 +1,332 @@
import React from 'react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import type { SxProps, Theme } from '@mui/material/styles';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import { WizardLayout } from '../../templates/WizardLayout';
import { ToggleButtonGroup } from '../../atoms/ToggleButtonGroup';
import { Collapse } from '../../atoms/Collapse';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Payment plan choice */
export type PaymentPlan = 'full' | 'deposit' | null;
/** Payment method choice */
export type PaymentMethod = 'Card' | 'Bank' | null;
/** Form values for the payment step */
export interface PaymentStepValues {
/** Full payment or deposit */
paymentPlan: PaymentPlan;
/** Card or bank transfer */
paymentMethod: PaymentMethod;
/** Terms accepted */
termsAccepted: boolean;
}
/** Field-level error messages */
export interface PaymentStepErrors {
paymentPlan?: string;
paymentMethod?: string;
termsAccepted?: string;
card?: string;
}
/** Props for the PaymentStep page component */
export interface PaymentStepProps {
/** Current form values */
values: PaymentStepValues;
/** Callback when any field value changes */
onChange: (values: PaymentStepValues) => void;
/** Callback when Confirm Payment is clicked */
onConfirmPayment: () => void;
/** Callback for back navigation */
onBack?: () => void;
/** Field-level validation errors */
errors?: PaymentStepErrors;
/** Whether the button is in a loading/processing state */
loading?: boolean;
/** Total amount */
totalPrice: number;
/** Deposit amount */
depositAmount?: number;
/** Bank account details for transfer */
bankDetails?: {
accountName: string;
bsb: string;
accountNumber: string;
reference?: string;
};
/** Card payment iframe slot (PayWay integration) */
cardFormSlot?: React.ReactNode;
/** Navigation bar */
navigation?: React.ReactNode;
/** Progress stepper */
progressStepper?: React.ReactNode;
/** Hide the help bar */
hideHelpBar?: boolean;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Step 14 — Payment for the FA arrangement wizard.
*
* Collect payment via credit card or bank transfer. Supports full
* payment or deposit. Hidden for pre-planning users.
*
* Payment plan shown before method so users know the amount first.
* Card details via PayWay iframe (slot). Bank transfer shows account info.
*
* Pure presentation component — props in, callbacks out.
*
* Spec: documentation/steps/steps/14_payment.yaml
*/
export const PaymentStep: React.FC<PaymentStepProps> = ({
values,
onChange,
onConfirmPayment,
onBack,
errors,
loading = false,
totalPrice,
depositAmount = 2000,
bankDetails = {
accountName: 'Funeral Arranger Services Pty Ltd',
bsb: '112-879',
accountNumber: '481 449 385',
},
cardFormSlot,
navigation,
progressStepper,
hideHelpBar,
sx,
}) => {
const payingAmount = values.paymentPlan === 'deposit' ? depositAmount : totalPrice;
return (
<WizardLayout
variant="centered-form"
navigation={navigation}
progressStepper={progressStepper}
showBackLink={!!onBack}
backLabel="Back"
onBack={onBack}
hideHelpBar={hideHelpBar}
sx={sx}
>
{/* Page heading */}
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
Payment
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 4 }}>
<LockOutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
<Typography variant="body2" color="text.secondary">
Your payment is processed securely. You won&apos;t be charged more than the total shown.
</Typography>
</Box>
{/* ─── Amount display ─── */}
<Paper
elevation={2}
sx={{
p: 3,
mb: 4,
textAlign: 'center',
borderRadius: 2,
}}
>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
{values.paymentPlan === 'deposit' ? 'Deposit amount' : 'Total due'}
</Typography>
<Typography variant="display3" color="primary" aria-live="polite" aria-atomic="true">
${payingAmount.toLocaleString('en-AU')}
</Typography>
{values.paymentPlan === 'deposit' && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
Remaining balance: ${(totalPrice - depositAmount).toLocaleString('en-AU')}
</Typography>
)}
</Paper>
<Box
component="form"
noValidate
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
onConfirmPayment();
}}
>
{/* ─── Payment plan ─── */}
<Box sx={{ mb: 4 }}>
<ToggleButtonGroup
label="Payment option"
options={[
{
value: 'full',
label: 'Pay in full',
description: `Pay $${totalPrice.toLocaleString('en-AU')} now`,
},
{
value: 'deposit',
label: 'Pay a deposit',
description: `Pay $${depositAmount.toLocaleString('en-AU')} now, balance arranged with provider`,
},
]}
value={values.paymentPlan}
onChange={(v) => onChange({ ...values, paymentPlan: v as PaymentPlan })}
error={!!errors?.paymentPlan}
helperText={errors?.paymentPlan}
required
fullWidth
/>
</Box>
{/* ─── Payment method ─── */}
<Box sx={{ mb: 3 }}>
<ToggleButtonGroup
label="Payment method"
options={[
{ value: 'Card', label: 'Credit or debit card' },
{ value: 'Bank', label: 'Bank transfer' },
]}
value={values.paymentMethod}
onChange={(v) => onChange({ ...values, paymentMethod: v as PaymentMethod })}
error={!!errors?.paymentMethod}
helperText={errors?.paymentMethod}
required
fullWidth
/>
</Box>
{/* ─── Card form (PayWay iframe slot) ─── */}
<Collapse in={values.paymentMethod === 'Card'}>
<Box sx={{ mb: 3, minHeight: 200 }} role="region" aria-label="Payment details">
{cardFormSlot || (
<Paper
variant="outlined"
sx={{
p: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 200,
bgcolor: 'var(--fa-color-surface-subtle)',
}}
>
<Typography variant="body2" color="text.secondary">
Secure card payment form will appear here
</Typography>
</Paper>
)}
{errors?.card && (
<Typography variant="body2" color="error" sx={{ mt: 1 }} role="alert">
{errors.card}
</Typography>
)}
</Box>
</Collapse>
{/* ─── Bank transfer details ─── */}
<Collapse in={values.paymentMethod === 'Bank'}>
<Paper
variant="outlined"
sx={{ p: 3, mb: 3 }}
role="region"
aria-label="Bank transfer details"
>
<Typography variant="h5" sx={{ mb: 2 }}>
Bank transfer details
</Typography>
<Box component="dl" sx={{ m: 0 }}>
{[
{ label: 'Account name', value: bankDetails.accountName },
{ label: 'BSB', value: bankDetails.bsb },
{ label: 'Account number', value: bankDetails.accountNumber },
].map(({ label, value }) => (
<Box key={label} sx={{ display: 'flex', justifyContent: 'space-between', py: 1 }}>
<Typography component="dt" variant="body2" color="text.secondary">
{label}
</Typography>
<Typography component="dd" variant="body1" sx={{ m: 0, fontFamily: 'monospace' }}>
{value}
</Typography>
</Box>
))}
</Box>
{bankDetails.reference && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
Reference: {bankDetails.reference}
</Typography>
)}
</Paper>
</Collapse>
{/* ─── Terms checkbox ─── */}
<FormControlLabel
control={
<Checkbox
checked={values.termsAccepted}
onChange={(e) => onChange({ ...values, termsAccepted: e.target.checked })}
/>
}
label={
<Typography variant="body2">
I agree to the{' '}
<Box
component="a"
href="#"
sx={{
color: 'var(--fa-color-text-brand)',
textDecoration: 'underline',
fontSize: 'inherit',
}}
>
service agreement
</Box>{' '}
and{' '}
<Box
component="a"
href="#"
sx={{
color: 'var(--fa-color-text-brand)',
textDecoration: 'underline',
fontSize: 'inherit',
}}
>
privacy policy
</Box>
</Typography>
}
sx={{ mb: 1, alignItems: 'flex-start', '& .MuiCheckbox-root': { pt: 0.5 } }}
/>
{errors?.termsAccepted && (
<Typography variant="body2" color="error" sx={{ mb: 2, ml: 4 }} role="alert">
{errors.termsAccepted}
</Typography>
)}
<Divider sx={{ my: 3 }} />
{/* CTA */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button type="submit" variant="contained" size="large" loading={loading}>
Confirm payment
</Button>
</Box>
</Box>
</WizardLayout>
);
};
PaymentStep.displayName = 'PaymentStep';
export default PaymentStep;

View File

@@ -0,0 +1,8 @@
export { PaymentStep, default } from './PaymentStep';
export type {
PaymentStepProps,
PaymentStepValues,
PaymentStepErrors,
PaymentPlan,
PaymentMethod,
} from './PaymentStep';