2026-02-24 11:09:35 +09:00

201 lines
6.1 KiB
TypeScript

/**
* GetStartedView - Main view for the get-started flow
*
* Supports multiple handoff scenarios from eligibility check:
*
* 1. Verified handoff (?verified=true):
* - User already completed OTP on eligibility page
* - SessionStorage has: sessionToken, accountStatus, prefill, email
* - Skip directly to complete-account step
*
* 2. Unverified handoff (?handoff=true):
* - User came from eligibility check success page or SF email
* - Email is pre-filled but NOT verified yet
* - User must complete OTP verification
* - SessionStorage may have: handoff-token for prefill data
*/
"use client";
import { useState, useCallback, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import { AuthLayout } from "@/components/templates/AuthLayout";
import { GetStartedForm } from "../components";
import { getSafeRedirect } from "@/features/auth/utils/route-protection";
import {
useGetStartedStore,
type GetStartedStep,
type GetStartedAddress,
} from "../stores/get-started.store";
import type { AccountStatus, VerifyCodeResponse } from "@customer-portal/domain/get-started";
// Session data staleness threshold (15 minutes)
const SESSION_STALE_THRESHOLD_MS = 15 * 60 * 1000;
// SessionStorage key constants
const SESSION_KEY_TOKEN = "get-started-session-token";
const SESSION_KEY_ACCOUNT_STATUS = "get-started-account-status";
const SESSION_KEY_PREFILL = "get-started-prefill";
const SESSION_KEY_EMAIL = "get-started-email";
const SESSION_KEY_TIMESTAMP = "get-started-timestamp";
const SESSION_KEY_HANDOFF_TOKEN = "get-started-handoff-token";
function clearGetStartedSessionStorage(): void {
sessionStorage.removeItem(SESSION_KEY_TOKEN);
sessionStorage.removeItem(SESSION_KEY_ACCOUNT_STATUS);
sessionStorage.removeItem(SESSION_KEY_PREFILL);
sessionStorage.removeItem(SESSION_KEY_EMAIL);
sessionStorage.removeItem(SESSION_KEY_TIMESTAMP);
sessionStorage.removeItem(SESSION_KEY_HANDOFF_TOKEN);
}
interface SessionData {
sessionToken: string | null;
accountStatus: string | null;
prefillRaw: string | null;
email: string | null;
timestamp: string | null;
}
function readVerifiedHandoffData(): SessionData {
return {
sessionToken: sessionStorage.getItem(SESSION_KEY_TOKEN),
accountStatus: sessionStorage.getItem(SESSION_KEY_ACCOUNT_STATUS),
prefillRaw: sessionStorage.getItem(SESSION_KEY_PREFILL),
email: sessionStorage.getItem(SESSION_KEY_EMAIL),
timestamp: sessionStorage.getItem(SESSION_KEY_TIMESTAMP),
};
}
function isSessionStale(timestamp: string | null): boolean {
if (!timestamp) return true;
return Date.now() - Number.parseInt(timestamp, 10) > SESSION_STALE_THRESHOLD_MS;
}
function parsePrefillData(prefillRaw: string | null): VerifyCodeResponse["prefill"] | null {
if (!prefillRaw) return null;
try {
return JSON.parse(prefillRaw);
} catch {
return null;
}
}
export function GetStartedView(): React.JSX.Element {
const searchParams = useSearchParams();
const {
updateFormData,
goToStep,
setHandoffToken,
setSessionToken,
setAccountStatus,
setPrefill,
setRedirectTo,
} = useGetStartedStore();
const [meta, setMeta] = useState({
title: "Get Started",
subtitle: "Enter your email to begin",
});
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (initialized) return;
handleRedirectParam();
const handledVerified = handleVerifiedHandoff();
if (!handledVerified) {
handleUnverifiedHandoff();
}
setInitialized(true);
function handleRedirectParam(): void {
const redirectParam = searchParams.get("redirect");
if (redirectParam) {
const safeRedirect = getSafeRedirect(redirectParam, "/account/dashboard");
setRedirectTo(safeRedirect);
}
}
function handleVerifiedHandoff(): boolean {
const verifiedParam = searchParams.get("verified");
if (verifiedParam !== "true") return false;
const sessionData = readVerifiedHandoffData();
clearGetStartedSessionStorage();
if (!sessionData.sessionToken || isSessionStale(sessionData.timestamp)) {
return false;
}
setSessionToken(sessionData.sessionToken);
if (sessionData.accountStatus) {
setAccountStatus(sessionData.accountStatus as AccountStatus);
}
const prefill = parsePrefillData(sessionData.prefillRaw);
if (prefill) {
setPrefill(prefill);
updateFormData({
email: sessionData.email || prefill.email || "",
firstName: prefill.firstName || "",
lastName: prefill.lastName || "",
phone: prefill.phone || "",
address: (prefill.address as GetStartedAddress) || {},
});
} else if (sessionData.email) {
updateFormData({ email: sessionData.email });
}
goToStep("complete-account");
return true;
}
function handleUnverifiedHandoff(): void {
const emailParam = searchParams.get("email");
const handoffParam = searchParams.get("handoff");
const storedHandoffToken = sessionStorage.getItem(SESSION_KEY_HANDOFF_TOKEN);
const storedEmail = sessionStorage.getItem(SESSION_KEY_EMAIL);
sessionStorage.removeItem(SESSION_KEY_HANDOFF_TOKEN);
sessionStorage.removeItem(SESSION_KEY_EMAIL);
const email = emailParam || storedEmail;
const isHandoff = handoffParam === "true" || !!storedHandoffToken;
if (email && isHandoff) {
updateFormData({ email });
if (storedHandoffToken) {
setHandoffToken(storedHandoffToken);
}
} else if (email) {
updateFormData({ email });
}
}
}, [
initialized,
searchParams,
updateFormData,
setHandoffToken,
goToStep,
setSessionToken,
setAccountStatus,
setPrefill,
setRedirectTo,
]);
const handleStepChange = useCallback(
(_step: GetStartedStep, stepMeta: { title: string; subtitle: string }) => {
setMeta(stepMeta);
},
[]
);
return (
<AuthLayout title={meta.title} subtitle={meta.subtitle} wide>
<GetStartedForm onStepChange={handleStepChange} />
</AuthLayout>
);
}
export default GetStartedView;