"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Icon, Avatar } from "./icons"; import { Spinner, StepBack, AccountPill, FlashNote, MethodRow, RememberDevice, OtpBoxes, ResendLink, SocialButtons, startSocial, PasswordStrength, } from "./bits"; import { lookupAccount, type Account } from "./data"; type Step = | "identify" | "connecting" | "notfound" | "primary" | "passkey" | "password" | "otp" | "another" | "totp" | "push" | "error" | "terms" | "success" | "recovery" | "fp_confirm" | "fp_code" | "fp_reset"; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export function LoginFlow() { const router = useRouter(); const [step, setStep] = useState("identify"); const [email, setEmail] = useState(""); const [account, setAccount] = useState(null); const [provider, setProvider] = useState(""); const [remember, setRemember] = useState(false); const [flash, setFlash] = useState(""); /* ---- history hash sync ---- */ function push(s: Step) { setStep(s); try { window.history.pushState({ s }, "", `#${s}`); } catch { /* ignore */ } } function replace(s: Step) { setStep(s); try { window.history.replaceState({ s }, "", `#${s}`); } catch { /* ignore */ } } const back = () => window.history.back(); function switchAccount() { setAccount(null); setEmail(""); setFlash(""); replace("identify"); } useEffect(() => { const onPop = () => { const h = window.location.hash.replace("#", "") as Step; setStep(h || "identify"); }; window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); }, []); /* ---- auth resolution ---- */ function afterAuth(factor: "password" | "passkey" | "otp" | "totp" | "push" | "social") { if (factor === "password") { setFlash(""); push("otp"); return; } let accepted = false; try { accepted = !!localStorage.getItem("lup_terms"); } catch { /* ignore */ } push(accepted ? "success" : "terms"); } function onSocial(p: "google" | "microsoft" | "apple") { const real = startSocial(p); if (real) return; // redirected away setProvider(p); push("connecting"); setTimeout(() => afterAuth("social"), 1300); } function identifyEmail() { if (!EMAIL_RE.test(email)) return; push("connecting"); setProvider(""); setTimeout(() => { const acc = lookupAccount(email); if (!acc) { replace("notfound"); return; } setAccount(acc); replace("primary"); }, 650); } /* =================================================================== */ return (
{step === "identify" && router.push("/portal/register")} />} {step === "connecting" && (

{provider ? `Connecting to ${cap(provider)}…` : "Looking up your account…"}

{provider &&

Demo mode — no provider keys configured.

}
)} {step === "notfound" && (
No account found for  {email}

We couldn't find that account

No account is on file for this email. Try another email or register a new account.

)} {step === "primary" && account && (

Hi {account.firstName}

Choose how you want to sign in.

replace("identify")} />
{account.hasPasskey && push("passkey")} />} push("password")} /> push("another")} />
)} {step === "passkey" && account && ( replace("password")} onAnother={() => replace("another")} onSuccess={() => afterAuth("passkey")} onFail={() => { setFlash("Passkey was cancelled or timed out."); replace("error"); }} /> )} {step === "password" && account && ( push("fp_confirm")} onOtp={() => replace("otp")} onLocked={() => setFlash("This account is temporarily locked.")} onOk={() => afterAuth("password")} /> )} {step === "otp" && account && ( afterAuth("otp")} /> )} {step === "another" && account && ( push(s)} /> )} {step === "totp" && account && ( afterAuth("totp")} /> )} {step === "push" && ( afterAuth("push")} onCancel={() => { setFlash("Sign-in request cancelled."); replace("another"); }} /> )} {step === "error" && (

Couldn't sign you in

{flash || "Something interrupted the sign-in."}

  • Make sure you're using the correct device or account.
  • Check your internet connection and try again.
  • You can switch to another verification method.
)} {step === "terms" && { try { localStorage.setItem("lup_terms", "1"); } catch { /* ignore */ } replace("success"); }} />} {step === "success" && ( router.push("/dashboard")} /> )} {step === "recovery" && (

Account recovery

None of the available methods worked. Our team can help verify your identity and restore access.

Contact support at help@lynkeduppro.com with your property or account details.
)} {step === "fp_confirm" && account && ( push("fp_code")} /> )} {step === "fp_code" && ( push("fp_reset")} /> )} {step === "fp_reset" && ( { setFlash("Password updated. Please sign in."); replace("password"); }} /> )}
); } /* ============================ screens ============================ */ function Identify({ email, setEmail, onSocial, onEmail, toRegister }: { email: string; setEmail: (v: string) => void; onSocial: (p: "google" | "microsoft" | "apple") => void; onEmail: () => void; toRegister: () => void; }) { const [showEmail, setShowEmail] = useState(false); const valid = EMAIL_RE.test(email); return (
Welcome back

Sign in to LynkedUp

Drone inspections, AI estimates and insurance-ready reports — all in one place.

or continue with email
{!showEmail ? ( ) : (
{ e.preventDefault(); onEmail(); }}>
setEmail(e.target.value)} placeholder="you@example.com" />
)}

New homeowner or contractor?

256-bit SSL Licensed & Insured Drone-Powered

Demo: any email signs in; an email starting with “new” shows “not found”.

); } function Passkey({ account, onBack, onPassword, onAnother, onSuccess, onFail }: { account: Account; onBack: () => void; onPassword: () => void; onAnother: () => void; onSuccess: () => void; onFail: () => void; }) { const [waiting, setWaiting] = useState(false); async function attempt() { setWaiting(true); try { if (navigator.credentials && "get" in navigator.credentials) { await navigator.credentials.get({ publicKey: { challenge: new Uint8Array(32), rpId: window.location.hostname, userVerification: "preferred", timeout: 8000 }, } as CredentialRequestOptions).catch(() => null); } } catch { /* ignore */ } setTimeout(onSuccess, 900); // demo: succeed } return (

Use your passkey

Verify it's you with your device's fingerprint, face or screen lock.

{waiting ? (

Waiting for your device…

) : ( )}

Demo: passkey always succeeds here.

); } function Password({ account, flash, onBack, onForgot, onOtp, onLocked, onOk }: { account: Account; flash: string; onBack: () => void; onForgot: () => void; onOtp: () => void; onLocked: () => void; onOk: () => void; }) { const [pw, setPw] = useState(""); const [show, setShow] = useState(false); const [err, setErr] = useState(""); function submit(e: React.FormEvent) { e.preventDefault(); if (pw.toLowerCase() === "wrong") { setErr("locked"); onLocked(); return; } if (!pw) return; onOk(); } return (

Enter your password

Signing in as {account.maskedEmail}

{flash &&
{flash}
} {err === "locked" &&
Account temporarily locked after too many attempts. .
}
setPw(e.target.value)} placeholder="••••••••" />

Demo: any password works; type “wrong” to see the lockout. Password always needs 2-step next.

); } function OtpVerify({ account, remember, setRemember, onBack, onVerified }: { account: Account; remember: boolean; setRemember: (v: boolean) => void; onBack: () => void; onVerified: () => void; }) { const [channel, setChannel] = useState<"email" | "sms" | "wa">("email"); const [error, setError] = useState(false); const target = channel === "email" ? account.maskedEmail : channel === "sms" ? account.maskedPhone : account.maskedWa; function complete(code: string) { if (code === "000000") { setError(true); return; } setError(false); onVerified(); } return (

2-step verification

Enter the 6-digit code we sent you to finish signing in.

Code sent to {target}

{error &&
Incorrect code. Please try again.
}

Demo: any 6 digits verify; “000000” shows an error.

); } function AnotherWay({ account, onBack, go }: { account: Account; onBack: () => void; go: (s: Step) => void }) { const [more, setMore] = useState(false); return (

Try another way

Choose a different way to verify it's you.

{account.hasPasskey && go("passkey")} />} {account.hasPush && go("push")} />} go("password")} /> {account.hasTotp && go("totp")} />} go("otp")} /> {more && <> go("otp")} /> go("otp")} /> }
{!more ? : }
); } function Totp({ remember, setRemember, onBack, onVerified }: { remember: boolean; setRemember: (v: boolean) => void; onBack: () => void; onVerified: () => void }) { const [setupKey, setSetupKey] = useState(false); const [error, setError] = useState(false); const [key, setKey] = useState(""); return (

Authenticator code

Open your authenticator app and enter the 6-digit code.

{!setupKey ? ( { if (c === "000000") setError(true); else onVerified(); }} error={error} /> ) : (
setKey(e.target.value)} placeholder="xxxx-xxxx-xxxx" />
)}
{error &&
Incorrect code.
}

Demo: any 6 digits verify; “000000” fails.

); } function PushApprove({ onBack, onApproved, onCancel }: { onBack: () => void; onApproved: () => void; onCancel: () => void }) { const [approved, setApproved] = useState(false); useEffect(() => { const t = setTimeout(() => { setApproved(true); setTimeout(onApproved, 900); }, 3200); return () => clearTimeout(t); }, [onApproved]); return (

{approved ? "Approved — signing you in…" : "Check your phone"}

{approved ? "Your sign-in was approved." : "We sent a notification to your mobile app. Approve it to continue."}

{approved ? : }
{!approved && }

Demo: auto-approves after ~3 seconds.

); } function TermsGate({ onAccept }: { onAccept: () => void }) { const [reviewed, setReviewed] = useState(false); const [checked, setChecked] = useState(false); const scrollRef = useRef(null); // If the terms fit without scrolling, enable the checkbox right away. useEffect(() => { const el = scrollRef.current; if (el && el.scrollHeight <= el.clientHeight + 12) setReviewed(true); }, []); function onScroll(e: React.UIEvent) { const el = e.currentTarget; if (el.scrollTop + el.clientHeight >= el.scrollHeight - 12) setReviewed(true); } return (

Before you continue

Please review and accept the portal terms to finish signing in.

{["Authorised use", "Accuracy of records", "Data & privacy", "Security", "Acceptable conduct"].map((h, i) => (

{i + 1}. {h}

By using the Lynkedup Pro portal you agree to access only records you are entitled to, keep your information accurate, protect your credentials, and use the service lawfully. Verification is required to safeguard your account.

))}

— End of terms —

); } function Success({ account, email, remember, onContinue }: { account: Account | null; email: string; remember: boolean; onContinue: () => void }) { useEffect(() => { if (remember) { try { localStorage.setItem("lup_trusted", String(Date.now() + 90 * 864e5)); } catch { /* ignore */ } } const t = setTimeout(onContinue, 2600); return () => clearTimeout(t); }, [remember, onContinue]); return (

You're signed in

Welcome back to the Lynkedup Pro portal.

{email || account?.name}
{remember &&

We'll remember this device for 90 days.

}
); } /* ---- forgot password mini machine ---- */ function ForgotConfirm({ masked, onBack, onSend }: { masked: string; onBack: () => void; onSend: () => void }) { return (

Reset your password

We'll send a verification code to {masked}.

); } function ForgotCode({ onBack, onOk }: { onBack: () => void; onOk: () => void }) { const [error, setError] = useState(false); return (

Enter the code

Enter the 6-digit code we just sent.

(c === "000000" ? setError(true) : onOk())} error={error} />
{error &&
Incorrect code.
}

Demo: any 6 digits work; “000000” fails.

); } function ForgotReset({ onDone }: { onDone: () => void }) { const [pw, setPw] = useState(""); const [confirm, setConfirm] = useState(""); const okMatch = pw.length > 0 && pw === confirm; const strongEnough = pw.length >= 8; return (

Create a new password

Choose a strong password you haven't used before.

setPw(e.target.value)} placeholder="New password" />
setConfirm(e.target.value)} placeholder="Re-enter password" />
{confirm && !okMatch &&

Passwords do not match.

}
); } /* ---- helpers ---- */ function CenterIcon({ name, tone }: { name: string; tone: "amber" | "green" | "blue" }) { const bg = tone === "green" ? "rgba(52,211,153,.14)" : tone === "blue" ? "rgba(56,189,248,.14)" : "var(--primary-ghost)"; const fg = tone === "green" ? "#6ee7b7" : tone === "blue" ? "#7dd3fc" : "var(--primary-2)"; return ( ); } function cap(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); }