- Centered-form layout with 3 progressive sub-steps: SSO/email → details → verify
- Google/Microsoft SSO buttons + email entry (sub-step 1)
- Name, phone, contact preference fields (sub-step 2) — phone optional when email-only
- 6-digit verification code entry (sub-step 3)
- Benefit framing ("Save your plan") not gate framing
- Responsive name fields (stacked on mobile, side-by-side on desktop)
- autoComplete + inputMode on all fields per WCAG 3.3.8
- Audit: 18/20 (Excellent), P1 fixed (responsive name fields)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
270 lines
8.5 KiB
TypeScript
270 lines
8.5 KiB
TypeScript
import { useState } from 'react';
|
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
import { AuthGateStep } from './AuthGateStep';
|
|
import type { AuthGateStepValues, AuthGateStepErrors } from './AuthGateStep';
|
|
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: AuthGateStepValues = {
|
|
subStep: 'email',
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
phone: '',
|
|
contactPreference: 'call_anytime',
|
|
verificationCode: '',
|
|
};
|
|
|
|
// ─── Meta ────────────────────────────────────────────────────────────────────
|
|
|
|
const meta: Meta<typeof AuthGateStep> = {
|
|
title: 'Pages/AuthGateStep',
|
|
component: AuthGateStep,
|
|
tags: ['autodocs'],
|
|
parameters: {
|
|
layout: 'fullscreen',
|
|
},
|
|
};
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof AuthGateStep>;
|
|
|
|
// ─── Interactive (default) ──────────────────────────────────────────────────
|
|
|
|
/** Fully interactive — walk through all three sub-steps */
|
|
export const Default: Story = {
|
|
render: () => {
|
|
const [values, setValues] = useState<AuthGateStepValues>({ ...defaultValues });
|
|
const [errors, setErrors] = useState<AuthGateStepErrors>({});
|
|
|
|
const handleContinue = () => {
|
|
const newErrors: AuthGateStepErrors = {};
|
|
|
|
if (values.subStep === 'email') {
|
|
if (!values.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
|
|
newErrors.email =
|
|
"That email address doesn't look quite right. Please check it and try again.";
|
|
}
|
|
if (Object.keys(newErrors).length === 0) {
|
|
setValues((v) => ({ ...v, subStep: 'details' }));
|
|
setErrors({});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (values.subStep === 'details') {
|
|
if (!values.firstName) newErrors.firstName = 'We need your first name to save the plan.';
|
|
if (!values.lastName) newErrors.lastName = 'We need your last name to save the plan.';
|
|
if (values.contactPreference !== 'email_only' && !values.phone) {
|
|
newErrors.phone = 'Please enter a valid Australian phone number, like 0412 345 678.';
|
|
}
|
|
if (Object.keys(newErrors).length === 0) {
|
|
setValues((v) => ({ ...v, subStep: 'verify' }));
|
|
setErrors({});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (values.subStep === 'verify') {
|
|
if (!values.verificationCode || values.verificationCode.length !== 6) {
|
|
newErrors.verificationCode =
|
|
"That code doesn't match. Please check the email we sent and try again.";
|
|
}
|
|
if (Object.keys(newErrors).length === 0) {
|
|
alert(`Authenticated: ${values.firstName} ${values.lastName} (${values.email})`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
};
|
|
|
|
return (
|
|
<AuthGateStep
|
|
values={values}
|
|
onChange={(v) => {
|
|
setValues(v);
|
|
setErrors({});
|
|
}}
|
|
onContinue={handleContinue}
|
|
onBack={() => alert('Back to preview')}
|
|
onGoogleSSO={() => alert('Google SSO')}
|
|
onMicrosoftSSO={() => alert('Microsoft SSO')}
|
|
errors={errors}
|
|
navigation={nav}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── Sub-step 2: Details ────────────────────────────────────────────────────
|
|
|
|
/** Details sub-step — email entered, name/phone fields revealed */
|
|
export const DetailsSubStep: Story = {
|
|
render: () => {
|
|
const [values, setValues] = useState<AuthGateStepValues>({
|
|
...defaultValues,
|
|
subStep: 'details',
|
|
email: 'jane@example.com',
|
|
});
|
|
return (
|
|
<AuthGateStep
|
|
values={values}
|
|
onChange={setValues}
|
|
onContinue={() => setValues((v) => ({ ...v, subStep: 'verify' }))}
|
|
onBack={() => alert('Back')}
|
|
navigation={nav}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── Sub-step 3: Verification ───────────────────────────────────────────────
|
|
|
|
/** Verification sub-step — code entry */
|
|
export const VerifySubStep: Story = {
|
|
render: () => {
|
|
const [values, setValues] = useState<AuthGateStepValues>({
|
|
...defaultValues,
|
|
subStep: 'verify',
|
|
email: 'jane@example.com',
|
|
firstName: 'Jane',
|
|
lastName: 'Smith',
|
|
phone: '0412 345 678',
|
|
});
|
|
return (
|
|
<AuthGateStep
|
|
values={values}
|
|
onChange={setValues}
|
|
onContinue={() => alert('Verified!')}
|
|
onBack={() => alert('Back')}
|
|
navigation={nav}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── At-need variant ────────────────────────────────────────────────────────
|
|
|
|
/** At-need subheading copy variant */
|
|
export const AtNeed: Story = {
|
|
render: () => {
|
|
const [values, setValues] = useState<AuthGateStepValues>({ ...defaultValues });
|
|
return (
|
|
<AuthGateStep
|
|
values={values}
|
|
onChange={setValues}
|
|
onContinue={() => setValues((v) => ({ ...v, subStep: 'details' }))}
|
|
onBack={() => alert('Back')}
|
|
isAtNeed
|
|
navigation={nav}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── Email-only preference ──────────────────────────────────────────────────
|
|
|
|
/** Phone becomes optional when contact preference is email-only */
|
|
export const EmailOnlyPreference: Story = {
|
|
render: () => {
|
|
const [values, setValues] = useState<AuthGateStepValues>({
|
|
...defaultValues,
|
|
subStep: 'details',
|
|
email: 'jane@example.com',
|
|
contactPreference: 'email_only',
|
|
});
|
|
return (
|
|
<AuthGateStep
|
|
values={values}
|
|
onChange={setValues}
|
|
onContinue={() => setValues((v) => ({ ...v, subStep: 'verify' }))}
|
|
onBack={() => alert('Back')}
|
|
navigation={nav}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── Validation errors ──────────────────────────────────────────────────────
|
|
|
|
/** Details sub-step with all validation errors showing */
|
|
export const WithErrors: Story = {
|
|
render: () => {
|
|
const [values, setValues] = useState<AuthGateStepValues>({
|
|
...defaultValues,
|
|
subStep: 'details',
|
|
email: 'jane@example.com',
|
|
});
|
|
return (
|
|
<AuthGateStep
|
|
values={values}
|
|
onChange={setValues}
|
|
onContinue={() => {}}
|
|
errors={{
|
|
firstName: 'We need your first name to save the plan.',
|
|
lastName: 'We need your last name to save the plan.',
|
|
phone: 'Please enter a valid Australian phone number, like 0412 345 678.',
|
|
}}
|
|
onBack={() => alert('Back')}
|
|
navigation={nav}
|
|
/>
|
|
);
|
|
},
|
|
};
|
|
|
|
// ─── Loading state ──────────────────────────────────────────────────────────
|
|
|
|
/** Continue button in loading state */
|
|
export const Loading: Story = {
|
|
render: () => {
|
|
const [values, setValues] = useState<AuthGateStepValues>({
|
|
...defaultValues,
|
|
subStep: 'verify',
|
|
email: 'jane@example.com',
|
|
firstName: 'Jane',
|
|
lastName: 'Smith',
|
|
phone: '0412 345 678',
|
|
verificationCode: '123456',
|
|
});
|
|
return (
|
|
<AuthGateStep
|
|
values={values}
|
|
onChange={setValues}
|
|
onContinue={() => {}}
|
|
loading
|
|
navigation={nav}
|
|
/>
|
|
);
|
|
},
|
|
};
|