diff --git a/src/components/pages/AuthGateStep/AuthGateStep.stories.tsx b/src/components/pages/AuthGateStep/AuthGateStep.stories.tsx new file mode 100644 index 0000000..952aa05 --- /dev/null +++ b/src/components/pages/AuthGateStep/AuthGateStep.stories.tsx @@ -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 = () => ( + + + + +); + +const nav = ( + } + 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 = { + title: 'Pages/AuthGateStep', + component: AuthGateStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Fully interactive — walk through all three sub-steps */ +export const Default: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + const [errors, setErrors] = useState({}); + + 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 ( + { + 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({ + ...defaultValues, + subStep: 'details', + email: 'jane@example.com', + }); + return ( + 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({ + ...defaultValues, + subStep: 'verify', + email: 'jane@example.com', + firstName: 'Jane', + lastName: 'Smith', + phone: '0412 345 678', + }); + return ( + alert('Verified!')} + onBack={() => alert('Back')} + navigation={nav} + /> + ); + }, +}; + +// ─── At-need variant ──────────────────────────────────────────────────────── + +/** At-need subheading copy variant */ +export const AtNeed: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + 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({ + ...defaultValues, + subStep: 'details', + email: 'jane@example.com', + contactPreference: 'email_only', + }); + return ( + 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({ + ...defaultValues, + subStep: 'details', + email: 'jane@example.com', + }); + return ( + {}} + 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({ + ...defaultValues, + subStep: 'verify', + email: 'jane@example.com', + firstName: 'Jane', + lastName: 'Smith', + phone: '0412 345 678', + verificationCode: '123456', + }); + return ( + {}} + loading + navigation={nav} + /> + ); + }, +}; diff --git a/src/components/pages/AuthGateStep/AuthGateStep.tsx b/src/components/pages/AuthGateStep/AuthGateStep.tsx new file mode 100644 index 0000000..c7633fb --- /dev/null +++ b/src/components/pages/AuthGateStep/AuthGateStep.tsx @@ -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; +} + +// ─── 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 = ({ + 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 ( + + {/* Page heading */} + + Save your plan + + + + {getSubheading(isAtNeed)} + + + { + e.preventDefault(); + onContinue(); + }} + > + {/* ─── Sub-step 1: SSO + Email ─── */} + + + + + + + + or + + + + 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) ─── */} + + + + A few details to save your plan + + + + handleFieldChange('firstName', e.target.value)} + error={!!errors?.firstName} + helperText={errors?.firstName} + autoComplete="given-name" + fullWidth + required + disabled={values.subStep === 'verify'} + /> + handleFieldChange('lastName', e.target.value)} + error={!!errors?.lastName} + helperText={errors?.lastName} + autoComplete="family-name" + fullWidth + required + disabled={values.subStep === 'verify'} + /> + + + 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'} + /> + + handleFieldChange('contactPreference', e.target.value)} + fullWidth + disabled={values.subStep === 'verify'} + > + Call me anytime + Email is preferred + Only contact by email + + + + + {/* ─── Sub-step 3: Verification code ─── */} + + + + We've sent a 6-digit code to {values.email}. Please enter it + below. + + + 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 + /> + + + + {/* Terms */} + + By continuing, you agree to the{' '} + + terms and conditions + + . + + + + + {/* CTA */} + + + + + + ); +}; + +AuthGateStep.displayName = 'AuthGateStep'; +export default AuthGateStep; diff --git a/src/components/pages/AuthGateStep/index.ts b/src/components/pages/AuthGateStep/index.ts new file mode 100644 index 0000000..66193b8 --- /dev/null +++ b/src/components/pages/AuthGateStep/index.ts @@ -0,0 +1,8 @@ +export { AuthGateStep, default } from './AuthGateStep'; +export type { + AuthGateStepProps, + AuthGateStepValues, + AuthGateStepErrors, + AuthSubStep, + ContactPreference, +} from './AuthGateStep';