import React, { useState, useEffect } from "react"; import { CheckIcon, HomeIcon, PlusIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { AddressAutocomplete } from "./form-primitives"; import { Button } from "./button"; import { Input } from "./input"; import { Slider } from "./slider"; import { Spinner } from "./spinner"; import { ToggleGroup, ToggleGroupItem } from "./toggle-group"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, } from "./input-otp"; import { FormField } from "./signup-form-primitives"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const fmt = (v: number) => new Intl.NumberFormat("en-AU", { style: "currency", currency: "AUD", maximumFractionDigits: 0, }).format(v); const GOAL_MIN = 50_000; const GOAL_MAX = 4_000_000; const GOAL_STEP = 10_000; // --------------------------------------------------------------------------- // Shared OTP verification — used by both the phone and email verify steps // --------------------------------------------------------------------------- const OTP_LENGTH = 6; const RESEND_COUNTDOWN = 60; /** * 6-digit OTP entry state plus the resend countdown. Auto-submits via * `onVerify` once all digits are entered; `restartTimer` restarts the * countdown when the user requests a new code. */ function useOtpTimer(countdown: number, onVerify?: (code: string) => void) { const [otp, setOtp] = useState(""); const [timer, setTimer] = useState(countdown); useEffect(() => { if (timer <= 0) return; const id = setInterval(() => setTimer((t) => t - 1), 1000); return () => clearInterval(id); }, [timer]); const handleChange = (value: string) => { setOtp(value); if (value.length === OTP_LENGTH) onVerify?.(value); }; const restartTimer = () => setTimer(RESEND_COUNTDOWN); return { otp, handleChange, timer, restartTimer }; } /** The 6-slot OTP input with its inline error message. */ function OtpCodeFields({ otp, onChange, disabled, error, className, }: { otp: string; onChange: (value: string) => void; disabled?: boolean; error?: string; className?: string; }) { const invalid = !!error || undefined; return (
{[0, 1, 2].map((i) => ( ))} {[3, 4, 5].map((i) => ( ))} {error && (

{error}

)}
); } /** "Verifying…" spinner while submitting, otherwise the resend prompt. */ function VerifyStatus({ isLoading, timer, prompt, onResend, }: { isLoading?: boolean; timer: number; prompt: string; onResend: () => void; }) { if (isLoading) { return (
Verifying…
); } return (

{prompt}{" "} {timer > 0 ? ( Resend it in {timer}s ) : ( )}

); } // --------------------------------------------------------------------------- // Step 2 — Phone Verification // --------------------------------------------------------------------------- export interface PhoneVerifyStepProps { phone?: string; countdown?: number; error?: string; isLoading?: boolean; onVerify?: (code: string) => void; onResend?: () => void; } export function PhoneVerifyStep({ phone = "+61 412 345 789", countdown = 45, error = "", isLoading = false, onVerify, onResend, }: PhoneVerifyStepProps) { const { otp, handleChange, timer, restartTimer } = useOtpTimer( countdown, onVerify, ); const lastFour = phone.slice(-4); return (

We sent you a text.

Number we have ends in •••• {lastFour}.

{ restartTimer(); onResend?.(); }} />
); } // --------------------------------------------------------------------------- // Step 2 (email variant) — Email Verification // --------------------------------------------------------------------------- /** Masks the local part of an email, e.g. "jamie@example.com" → "j***e@example.com". */ function maskEmail(email: string) { const [local, domain] = email.split("@"); if (!domain || local.length <= 2) return email; return `${local[0]}***${local[local.length - 1]}@${domain}`; } export interface EmailVerifyStepProps { email?: string; countdown?: number; error?: string; isLoading?: boolean; onVerify?: (code: string) => void; onResend?: () => void; } export function EmailVerifyStep({ email = "jamie@example.com", countdown = 45, error = "", isLoading = false, onVerify, onResend, }: EmailVerifyStepProps) { const { otp, handleChange, timer, restartTimer } = useOtpTimer( countdown, onVerify, ); return (

We sent you an email.

Check {maskEmail(email)} for your verification code.

{ restartTimer(); onResend?.(); }} />
); } // --------------------------------------------------------------------------- // Step 3 — Request Broker Access // --------------------------------------------------------------------------- export interface BrokerFormValues { firstName: string; lastName: string; email: string; phone: string; } export interface BrokerRequestStepProps { onValuesChange?: (values: BrokerFormValues) => void; } export function BrokerRequestStep({ onValuesChange }: BrokerRequestStepProps) { const [values, setValues] = useState({ firstName: "", lastName: "", email: "", phone: "", }); const update = (field: keyof BrokerFormValues, value: string) => { setValues((prev) => { const next = { ...prev, [field]: value }; onValuesChange?.(next); return next; }); }; return (

You have access for the first 3 months covered by us. After this date you will need to be connected with a mortgage broker or financial planner to maintain access.

update("firstName", e.target.value)} /> update("lastName", e.target.value)} />
update("email", e.target.value)} /> update("phone", e.target.value)} />
); } // --------------------------------------------------------------------------- // Step 4 — Connect Bank // --------------------------------------------------------------------------- const BANKS = ["CBA", "ANZ", "NAB", "WBC", "MAC", "BOQ"]; export interface ConnectBankStepProps { onConnect?: () => void; } export function ConnectBankStep({ onConnect }: ConnectBankStepProps) { return (
); } // --------------------------------------------------------------------------- // Step 5 — Retrieve Bank Data (loading state) // --------------------------------------------------------------------------- export function RetrieveBankDataStep() { return (

Retrieving your bank data…

This may take a few seconds. Please don't close this window.

); } // --------------------------------------------------------------------------- // Step 6 — Connect Property // --------------------------------------------------------------------------- interface PropertyItem { id: string; address: string; suburb: string; state: string; } export interface ConnectPropertyStepProps { properties?: PropertyItem[]; onAddProperty?: (address: string) => void; onCanProceedChange?: (can: boolean) => void; onGoalChange?: (value: number) => void; } export function ConnectPropertyStep({ properties = [], onAddProperty, onCanProceedChange, onGoalChange, }: ConnectPropertyStepProps) { const [hasProperty, setHasProperty] = useState(null); const [addressValue, setAddressValue] = useState(""); const [goalValue, setGoalValue] = useState(2_000_000); const canProceed = hasProperty === false || (hasProperty === true && properties.length > 0); useEffect(() => { onCanProceedChange?.(canProceed); // eslint-disable-next-line react-hooks/exhaustive-deps }, [canProceed]); const handleGoalChange = (v: number) => { setGoalValue(v); onGoalChange?.(v); }; return (
{ const val = vals[0]; setHasProperty(val === "yes" ? true : val === "no" ? false : null); }} className="w-full" > Yes, I have a property No, I don't {hasProperty === true && ( <> { onAddProperty?.(opt.label); setAddressValue(""); }} /> {properties.length > 0 && (
    {properties.map((p) => (
  • ))}
)} )} {hasProperty === false && (

What's your property budget?

Set your estimated buying goal to help us tailor your experience.

{fmt(goalValue)}

)}
); } // --------------------------------------------------------------------------- // BuyingGoalStep — standalone step (used in template stories) // --------------------------------------------------------------------------- export interface BuyingGoalStepProps { initialValue?: number; isLoading?: boolean; onConfirm?: (value: number) => void; } export function BuyingGoalStep({ initialValue = 2_000_000, isLoading = false, onConfirm, }: BuyingGoalStepProps) { const [value, setValue] = useState(initialValue); return (

{fmt(value)}

setValue(v)} disabled={isLoading} aria-label="Buying goal" />
); } // --------------------------------------------------------------------------- // Step 7 — Success // --------------------------------------------------------------------------- export interface FrontendSuccessStepProps { firstName?: string; onGoToDashboard?: () => void; } export function FrontendSuccessStep({ firstName = "Alex", onGoToDashboard, }: FrontendSuccessStepProps) { return (

You're all set, {firstName}!

Your account is ready. Head to your dashboard to explore your financial overview and next steps.

); }