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:
184
src/components/pages/PaymentStep/PaymentStep.stories.tsx
Normal file
184
src/components/pages/PaymentStep/PaymentStep.stories.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
332
src/components/pages/PaymentStep/PaymentStep.tsx
Normal file
332
src/components/pages/PaymentStep/PaymentStep.tsx
Normal 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'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;
|
||||
8
src/components/pages/PaymentStep/index.ts
Normal file
8
src/components/pages/PaymentStep/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { PaymentStep, default } from './PaymentStep';
|
||||
export type {
|
||||
PaymentStepProps,
|
||||
PaymentStepValues,
|
||||
PaymentStepErrors,
|
||||
PaymentPlan,
|
||||
PaymentMethod,
|
||||
} from './PaymentStep';
|
||||
Reference in New Issue
Block a user