2026-01-14 13:54:01 +09:00
|
|
|
/**
|
|
|
|
|
* GetStartedView - Main view for the get-started flow
|
|
|
|
|
*
|
2026-01-14 17:14:07 +09:00
|
|
|
* 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
|
2026-01-14 13:54:01 +09:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useCallback, useEffect } from "react";
|
|
|
|
|
import { useSearchParams } from "next/navigation";
|
|
|
|
|
import { AuthLayout } from "@/components/templates/AuthLayout";
|
|
|
|
|
import { GetStartedForm } from "../components";
|
2026-01-19 10:13:55 +09:00
|
|
|
import { getSafeRedirect } from "@/features/auth/utils/route-protection";
|
2026-01-14 17:14:07 +09:00
|
|
|
import {
|
|
|
|
|
useGetStartedStore,
|
|
|
|
|
type GetStartedStep,
|
|
|
|
|
type GetStartedAddress,
|
|
|
|
|
} from "../stores/get-started.store";
|
|
|
|
|
import type { AccountStatus, VerifyCodeResponse } from "@customer-portal/domain/get-started";
|
|
|
|
|
|
2026-02-24 11:09:35 +09:00
|
|
|
// Session data staleness threshold (15 minutes)
|
|
|
|
|
const SESSION_STALE_THRESHOLD_MS = 15 * 60 * 1000;
|
2026-01-14 13:54:01 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
// 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 {
|
2026-01-14 13:54:01 +09:00
|
|
|
const searchParams = useSearchParams();
|
2026-01-14 17:14:07 +09:00
|
|
|
const {
|
|
|
|
|
updateFormData,
|
|
|
|
|
goToStep,
|
|
|
|
|
setHandoffToken,
|
|
|
|
|
setSessionToken,
|
|
|
|
|
setAccountStatus,
|
|
|
|
|
setPrefill,
|
2026-01-19 10:13:55 +09:00
|
|
|
setRedirectTo,
|
2026-01-14 17:14:07 +09:00
|
|
|
} = useGetStartedStore();
|
2026-01-14 13:54:01 +09:00
|
|
|
const [meta, setMeta] = useState({
|
|
|
|
|
title: "Get Started",
|
|
|
|
|
subtitle: "Enter your email to begin",
|
|
|
|
|
});
|
|
|
|
|
const [initialized, setInitialized] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (initialized) return;
|
|
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
handleRedirectParam();
|
|
|
|
|
const handledVerified = handleVerifiedHandoff();
|
|
|
|
|
if (!handledVerified) {
|
|
|
|
|
handleUnverifiedHandoff();
|
2026-01-19 10:13:55 +09:00
|
|
|
}
|
2026-02-03 13:12:08 +09:00
|
|
|
setInitialized(true);
|
2026-01-19 10:13:55 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
function handleRedirectParam(): void {
|
|
|
|
|
const redirectParam = searchParams.get("redirect");
|
|
|
|
|
if (redirectParam) {
|
|
|
|
|
const safeRedirect = getSafeRedirect(redirectParam, "/account/dashboard");
|
|
|
|
|
setRedirectTo(safeRedirect);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-14 17:14:07 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
function handleVerifiedHandoff(): boolean {
|
|
|
|
|
const verifiedParam = searchParams.get("verified");
|
|
|
|
|
if (verifiedParam !== "true") return false;
|
2026-01-14 17:14:07 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
const sessionData = readVerifiedHandoffData();
|
2026-01-14 17:14:07 +09:00
|
|
|
clearGetStartedSessionStorage();
|
|
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
if (!sessionData.sessionToken || isSessionStale(sessionData.timestamp)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-01-14 17:14:07 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
setSessionToken(sessionData.sessionToken);
|
|
|
|
|
if (sessionData.accountStatus) {
|
|
|
|
|
setAccountStatus(sessionData.accountStatus as AccountStatus);
|
|
|
|
|
}
|
2026-01-14 17:14:07 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
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 });
|
2026-01-14 17:14:07 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
goToStep("complete-account");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-01-14 13:54:01 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
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);
|
2026-01-14 13:54:01 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
sessionStorage.removeItem(SESSION_KEY_HANDOFF_TOKEN);
|
|
|
|
|
sessionStorage.removeItem(SESSION_KEY_EMAIL);
|
2026-01-14 17:14:07 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
const email = emailParam || storedEmail;
|
|
|
|
|
const isHandoff = handoffParam === "true" || !!storedHandoffToken;
|
2026-01-14 13:54:01 +09:00
|
|
|
|
2026-02-03 13:12:08 +09:00
|
|
|
if (email && isHandoff) {
|
|
|
|
|
updateFormData({ email });
|
|
|
|
|
if (storedHandoffToken) {
|
|
|
|
|
setHandoffToken(storedHandoffToken);
|
|
|
|
|
}
|
|
|
|
|
} else if (email) {
|
|
|
|
|
updateFormData({ email });
|
2026-01-14 17:14:07 +09:00
|
|
|
}
|
2026-01-14 13:54:01 +09:00
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
initialized,
|
|
|
|
|
searchParams,
|
|
|
|
|
updateFormData,
|
2026-01-14 17:14:07 +09:00
|
|
|
setHandoffToken,
|
2026-01-14 13:54:01 +09:00
|
|
|
goToStep,
|
2026-01-14 17:14:07 +09:00
|
|
|
setSessionToken,
|
2026-01-14 13:54:01 +09:00
|
|
|
setAccountStatus,
|
|
|
|
|
setPrefill,
|
2026-01-19 10:13:55 +09:00
|
|
|
setRedirectTo,
|
2026-01-14 13:54:01 +09:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
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;
|