Add AuthGateStep page (wizard step 5)
- 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>
This commit is contained in:
269
src/components/pages/AuthGateStep/AuthGateStep.stories.tsx
Normal file
269
src/components/pages/AuthGateStep/AuthGateStep.stories.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
336
src/components/pages/AuthGateStep/AuthGateStep.tsx
Normal file
336
src/components/pages/AuthGateStep/AuthGateStep.tsx
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles';
|
||||||
|
import GoogleIcon from '@mui/icons-material/Google';
|
||||||
|
import MicrosoftIcon from '@mui/icons-material/Window';
|
||||||
|
import { WizardLayout } from '../../templates/WizardLayout';
|
||||||
|
import { Collapse } from '../../atoms/Collapse';
|
||||||
|
import { Typography } from '../../atoms/Typography';
|
||||||
|
import { Button } from '../../atoms/Button';
|
||||||
|
import { Divider } from '../../atoms/Divider';
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Which sub-step of the auth flow the user is on */
|
||||||
|
export type AuthSubStep = 'email' | 'details' | 'verify';
|
||||||
|
|
||||||
|
/** Contact preference options */
|
||||||
|
export type ContactPreference = 'call_anytime' | 'email_preferred' | 'email_only';
|
||||||
|
|
||||||
|
/** Form values for the auth gate step */
|
||||||
|
export interface AuthGateStepValues {
|
||||||
|
/** Current sub-step */
|
||||||
|
subStep: AuthSubStep;
|
||||||
|
/** Email address */
|
||||||
|
email: string;
|
||||||
|
/** First name */
|
||||||
|
firstName: string;
|
||||||
|
/** Last name */
|
||||||
|
lastName: string;
|
||||||
|
/** Phone number */
|
||||||
|
phone: string;
|
||||||
|
/** Contact preference */
|
||||||
|
contactPreference: ContactPreference;
|
||||||
|
/** Email verification code */
|
||||||
|
verificationCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Field-level error messages */
|
||||||
|
export interface AuthGateStepErrors {
|
||||||
|
email?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
phone?: string;
|
||||||
|
verificationCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Props for the AuthGateStep page component */
|
||||||
|
export interface AuthGateStepProps {
|
||||||
|
/** Current form values */
|
||||||
|
values: AuthGateStepValues;
|
||||||
|
/** Callback when any field value changes */
|
||||||
|
onChange: (values: AuthGateStepValues) => void;
|
||||||
|
/** Callback when the Continue button is clicked */
|
||||||
|
onContinue: () => void;
|
||||||
|
/** Callback for back navigation */
|
||||||
|
onBack?: () => void;
|
||||||
|
/** Callback for Google SSO */
|
||||||
|
onGoogleSSO?: () => void;
|
||||||
|
/** Callback for Microsoft SSO */
|
||||||
|
onMicrosoftSSO?: () => void;
|
||||||
|
/** Field-level validation errors */
|
||||||
|
errors?: AuthGateStepErrors;
|
||||||
|
/** Whether the Continue button is in a loading state */
|
||||||
|
loading?: boolean;
|
||||||
|
/** Whether the user is arranging at-need (vs pre-planning) */
|
||||||
|
isAtNeed?: boolean;
|
||||||
|
/** Navigation bar — passed through to WizardLayout */
|
||||||
|
navigation?: React.ReactNode;
|
||||||
|
/** Hide the help bar */
|
||||||
|
hideHelpBar?: boolean;
|
||||||
|
/** MUI sx prop for the root */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Copy helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function getSubheading(isAtNeed: boolean): string {
|
||||||
|
if (isAtNeed) {
|
||||||
|
return 'We need a few details so a funeral arranger can help you with the next steps.';
|
||||||
|
}
|
||||||
|
return 'Save your plan to return and update it anytime.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCTALabel(subStep: AuthSubStep): string {
|
||||||
|
switch (subStep) {
|
||||||
|
case 'email':
|
||||||
|
return 'Continue with email';
|
||||||
|
case 'details':
|
||||||
|
return 'Continue';
|
||||||
|
case 'verify':
|
||||||
|
return 'Verify and continue';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Component ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 5 — Auth Gate for the FA arrangement wizard.
|
||||||
|
*
|
||||||
|
* Registration/login step positioned after preview (step 4). Users have
|
||||||
|
* already seen packages with pricing before being asked to register.
|
||||||
|
* Framed as a benefit ("Save your plan") not a gate.
|
||||||
|
*
|
||||||
|
* Three sub-steps with progressive disclosure:
|
||||||
|
* 1. SSO buttons + email entry
|
||||||
|
* 2. Name, phone, contact preference (after email)
|
||||||
|
* 3. Verification code (after details)
|
||||||
|
*
|
||||||
|
* Phone becomes optional when contactPreference is "email_only".
|
||||||
|
*
|
||||||
|
* Pure presentation component — props in, callbacks out.
|
||||||
|
*
|
||||||
|
* Spec: documentation/steps/steps/05_auth_gate.yaml
|
||||||
|
*/
|
||||||
|
export const AuthGateStep: React.FC<AuthGateStepProps> = ({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
onContinue,
|
||||||
|
onBack,
|
||||||
|
onGoogleSSO,
|
||||||
|
onMicrosoftSSO,
|
||||||
|
errors,
|
||||||
|
loading = false,
|
||||||
|
isAtNeed = false,
|
||||||
|
navigation,
|
||||||
|
hideHelpBar,
|
||||||
|
sx,
|
||||||
|
}) => {
|
||||||
|
const isEmailOnly = values.contactPreference === 'email_only';
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof AuthGateStepValues, value: string) => {
|
||||||
|
onChange({ ...values, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WizardLayout
|
||||||
|
variant="centered-form"
|
||||||
|
navigation={navigation}
|
||||||
|
showBackLink={!!onBack}
|
||||||
|
backLabel="Back"
|
||||||
|
onBack={onBack}
|
||||||
|
hideHelpBar={hideHelpBar}
|
||||||
|
sx={sx}
|
||||||
|
>
|
||||||
|
{/* Page heading */}
|
||||||
|
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
|
||||||
|
Save your plan
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 5 }} aria-live="polite">
|
||||||
|
{getSubheading(isAtNeed)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
noValidate
|
||||||
|
onSubmit={(e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onContinue();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ─── Sub-step 1: SSO + Email ─── */}
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, mb: 3 }}
|
||||||
|
role="group"
|
||||||
|
aria-label="Sign in options"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<GoogleIcon />}
|
||||||
|
onClick={onGoogleSSO}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Continue with Google
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<MicrosoftIcon />}
|
||||||
|
onClick={onMicrosoftSSO}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Continue with Microsoft
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
or
|
||||||
|
</Typography>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Your email address"
|
||||||
|
type="email"
|
||||||
|
value={values.email}
|
||||||
|
onChange={(e) => handleFieldChange('email', e.target.value)}
|
||||||
|
error={!!errors?.email}
|
||||||
|
helperText={errors?.email}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
autoComplete="email"
|
||||||
|
inputMode="email"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={values.subStep !== 'email'}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ─── Sub-step 2: Details (after email) ─── */}
|
||||||
|
<Collapse in={values.subStep === 'details' || values.subStep === 'verify'}>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', flexDirection: 'column', gap: 2.5, mb: 3 }}
|
||||||
|
role="group"
|
||||||
|
aria-label="Your details"
|
||||||
|
>
|
||||||
|
<Typography variant="labelLg" component="h2" sx={{ mb: 0.5 }}>
|
||||||
|
A few details to save your plan
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="First name"
|
||||||
|
value={values.firstName}
|
||||||
|
onChange={(e) => handleFieldChange('firstName', e.target.value)}
|
||||||
|
error={!!errors?.firstName}
|
||||||
|
helperText={errors?.firstName}
|
||||||
|
autoComplete="given-name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={values.subStep === 'verify'}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Last name"
|
||||||
|
value={values.lastName}
|
||||||
|
onChange={(e) => handleFieldChange('lastName', e.target.value)}
|
||||||
|
error={!!errors?.lastName}
|
||||||
|
helperText={errors?.lastName}
|
||||||
|
autoComplete="family-name"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={values.subStep === 'verify'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={isEmailOnly ? 'Phone (optional)' : 'Best number to reach you'}
|
||||||
|
type="tel"
|
||||||
|
value={values.phone}
|
||||||
|
onChange={(e) => handleFieldChange('phone', e.target.value)}
|
||||||
|
error={!!errors?.phone}
|
||||||
|
helperText={errors?.phone}
|
||||||
|
placeholder="e.g. 0412 345 678"
|
||||||
|
autoComplete="tel"
|
||||||
|
inputMode="tel"
|
||||||
|
fullWidth
|
||||||
|
required={!isEmailOnly}
|
||||||
|
disabled={values.subStep === 'verify'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
label="Contact preference"
|
||||||
|
value={values.contactPreference}
|
||||||
|
onChange={(e) => handleFieldChange('contactPreference', e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
disabled={values.subStep === 'verify'}
|
||||||
|
>
|
||||||
|
<MenuItem value="call_anytime">Call me anytime</MenuItem>
|
||||||
|
<MenuItem value="email_preferred">Email is preferred</MenuItem>
|
||||||
|
<MenuItem value="email_only">Only contact by email</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* ─── Sub-step 3: Verification code ─── */}
|
||||||
|
<Collapse in={values.subStep === 'verify'}>
|
||||||
|
<Box sx={{ mb: 3 }} role="group" aria-label="Email verification">
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
We've sent a 6-digit code to <strong>{values.email}</strong>. Please enter it
|
||||||
|
below.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Verification code"
|
||||||
|
value={values.verificationCode}
|
||||||
|
onChange={(e) => handleFieldChange('verificationCode', e.target.value)}
|
||||||
|
error={!!errors?.verificationCode}
|
||||||
|
helperText={errors?.verificationCode || 'Check your email for the 6-digit code'}
|
||||||
|
placeholder="Enter 6-digit code"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
inputMode="numeric"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* Terms */}
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 3 }}>
|
||||||
|
By continuing, you agree to the{' '}
|
||||||
|
<Box
|
||||||
|
component="a"
|
||||||
|
href="#"
|
||||||
|
sx={{
|
||||||
|
color: 'var(--fa-color-text-brand)',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
terms and conditions
|
||||||
|
</Box>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
|
{/* CTA */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button type="submit" variant="contained" size="large" loading={loading}>
|
||||||
|
{getCTALabel(values.subStep)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthGateStep.displayName = 'AuthGateStep';
|
||||||
|
export default AuthGateStep;
|
||||||
8
src/components/pages/AuthGateStep/index.ts
Normal file
8
src/components/pages/AuthGateStep/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { AuthGateStep, default } from './AuthGateStep';
|
||||||
|
export type {
|
||||||
|
AuthGateStepProps,
|
||||||
|
AuthGateStepValues,
|
||||||
|
AuthGateStepErrors,
|
||||||
|
AuthSubStep,
|
||||||
|
ContactPreference,
|
||||||
|
} from './AuthGateStep';
|
||||||
Reference in New Issue
Block a user