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-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";
|
|
|
|
|
|
|
|
|
|
// Session data staleness threshold (5 minutes)
|
|
|
|
|
const SESSION_STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
2026-01-14 13:54:01 +09:00
|
|
|
|
|
|
|
|
export function GetStartedView() {
|
|
|
|
|
const searchParams = useSearchParams();
|
2026-01-14 17:14:07 +09:00
|
|
|
const {
|
|
|
|
|
updateFormData,
|
|
|
|
|
goToStep,
|
|
|
|
|
setHandoffToken,
|
|
|
|
|
setSessionToken,
|
|
|
|
|
setAccountStatus,
|
|
|
|
|
setPrefill,
|
|
|
|
|
} = 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);
|
|
|
|
|
|
2026-01-14 17:14:07 +09:00
|
|
|
// Helper to clear all get-started sessionStorage items
|
|
|
|
|
const clearGetStartedSessionStorage = () => {
|
|
|
|
|
sessionStorage.removeItem("get-started-session-token");
|
|
|
|
|
sessionStorage.removeItem("get-started-account-status");
|
|
|
|
|
sessionStorage.removeItem("get-started-prefill");
|
|
|
|
|
sessionStorage.removeItem("get-started-email");
|
|
|
|
|
sessionStorage.removeItem("get-started-timestamp");
|
|
|
|
|
sessionStorage.removeItem("get-started-handoff-token");
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-14 13:54:01 +09:00
|
|
|
// Check for handoff from eligibility check on mount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (initialized) return;
|
|
|
|
|
|
2026-01-14 17:14:07 +09:00
|
|
|
// Check for verified handoff (user already completed OTP on eligibility page)
|
2026-01-14 13:54:01 +09:00
|
|
|
const verifiedParam = searchParams.get("verified");
|
|
|
|
|
|
2026-01-14 17:14:07 +09:00
|
|
|
if (verifiedParam === "true") {
|
|
|
|
|
// Read all session data at once
|
|
|
|
|
const storedSessionToken = sessionStorage.getItem("get-started-session-token");
|
|
|
|
|
const storedAccountStatus = sessionStorage.getItem("get-started-account-status");
|
|
|
|
|
const storedPrefillRaw = sessionStorage.getItem("get-started-prefill");
|
|
|
|
|
const storedEmail = sessionStorage.getItem("get-started-email");
|
|
|
|
|
const storedTimestamp = sessionStorage.getItem("get-started-timestamp");
|
|
|
|
|
|
|
|
|
|
// Validate timestamp to prevent stale data
|
|
|
|
|
const isStale =
|
2026-01-15 11:28:25 +09:00
|
|
|
!storedTimestamp ||
|
|
|
|
|
Date.now() - Number.parseInt(storedTimestamp, 10) > SESSION_STALE_THRESHOLD_MS;
|
2026-01-14 17:14:07 +09:00
|
|
|
|
|
|
|
|
// Clear sessionStorage immediately after reading
|
|
|
|
|
clearGetStartedSessionStorage();
|
|
|
|
|
|
|
|
|
|
if (storedSessionToken && !isStale) {
|
|
|
|
|
// Parse prefill data
|
|
|
|
|
let prefill: VerifyCodeResponse["prefill"] | null = null;
|
|
|
|
|
if (storedPrefillRaw) {
|
|
|
|
|
try {
|
|
|
|
|
prefill = JSON.parse(storedPrefillRaw);
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore parse errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set session data in store
|
|
|
|
|
setSessionToken(storedSessionToken);
|
|
|
|
|
if (storedAccountStatus) {
|
|
|
|
|
setAccountStatus(storedAccountStatus as AccountStatus);
|
|
|
|
|
}
|
|
|
|
|
if (prefill) {
|
|
|
|
|
setPrefill(prefill);
|
|
|
|
|
// Also update form data with prefill
|
|
|
|
|
updateFormData({
|
|
|
|
|
email: storedEmail || prefill.email || "",
|
|
|
|
|
firstName: prefill.firstName || "",
|
|
|
|
|
lastName: prefill.lastName || "",
|
|
|
|
|
phone: prefill.phone || "",
|
|
|
|
|
address: (prefill.address as GetStartedAddress) || {},
|
|
|
|
|
});
|
|
|
|
|
} else if (storedEmail) {
|
|
|
|
|
updateFormData({ email: storedEmail });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip directly to complete-account (email already verified)
|
|
|
|
|
goToStep("complete-account");
|
|
|
|
|
setInitialized(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// If stale or no token, fall through to normal flow
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for unverified handoff (from success page CTA or SF email)
|
|
|
|
|
const emailParam = searchParams.get("email");
|
|
|
|
|
const handoffParam = searchParams.get("handoff");
|
|
|
|
|
const storedHandoffToken = sessionStorage.getItem("get-started-handoff-token");
|
2026-01-14 13:54:01 +09:00
|
|
|
const storedEmail = sessionStorage.getItem("get-started-email");
|
|
|
|
|
|
2026-01-14 17:14:07 +09:00
|
|
|
// Clear handoff-related sessionStorage after reading
|
|
|
|
|
sessionStorage.removeItem("get-started-handoff-token");
|
2026-01-14 13:54:01 +09:00
|
|
|
sessionStorage.removeItem("get-started-email");
|
|
|
|
|
|
|
|
|
|
const email = emailParam || storedEmail;
|
2026-01-14 17:14:07 +09:00
|
|
|
const isHandoff = handoffParam === "true" || !!storedHandoffToken;
|
|
|
|
|
|
|
|
|
|
if (email && isHandoff) {
|
|
|
|
|
// User came from eligibility check - email is NOT verified yet
|
|
|
|
|
// Pre-fill email and let user verify via OTP
|
|
|
|
|
updateFormData({ email });
|
2026-01-14 13:54:01 +09:00
|
|
|
|
2026-01-14 17:14:07 +09:00
|
|
|
// Store handoff token if available - will be used during OTP verification
|
|
|
|
|
if (storedHandoffToken) {
|
|
|
|
|
setHandoffToken(storedHandoffToken);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stay at email step - user needs to verify their email
|
|
|
|
|
// Don't call goToStep - let the form start at its default step
|
|
|
|
|
// The email is pre-filled so user just clicks "Send Code"
|
|
|
|
|
} else if (email) {
|
|
|
|
|
// Just email param (from SF email link without handoff)
|
2026-01-14 13:54:01 +09:00
|
|
|
updateFormData({ email });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setInitialized(true);
|
|
|
|
|
}, [
|
|
|
|
|
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,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
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;
|