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 ─── */}
+
+ }
+ onClick={onGoogleSSO}
+ type="button"
+ >
+ Continue with Google
+
+ }
+ onClick={onMicrosoftSSO}
+ type="button"
+ >
+ Continue with Microsoft
+
+
+
+
+
+ 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'}
+ >
+
+
+
+
+
+
+
+ {/* ─── 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';