feat: refactor onboarding into a multi-step flow with dedicated routes and session state management

This commit is contained in:
Jose Selesan
2026-04-17 16:17:19 -03:00
parent 4588e63fa5
commit 20b70c06c1
12 changed files with 617 additions and 382 deletions

View File

@@ -0,0 +1,119 @@
import { OnboardingCompleteStep } from '@/features/onboard/components/onboarding-complete-step';
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
import {
clearOnboardingSessionState,
getOnboardingSessionState,
updateOnboardingSessionState,
} from '@/features/onboard/onboarding-session';
import type { CompleteValues } from '@/features/onboard/onboarding.types';
import { apiClient } from '@/lib/api-client';
import { useAuth } from '@/lib/auth';
import { setCurrentComplexSlug } from '@/lib/current-complex';
import { zodResolver } from '@hookform/resolvers/zod';
import { type PlanSummary, onboardingCompleteSchema } from '@repo/api-contract';
import { useNavigate } from '@tanstack/react-router';
import { useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
export function OnboardCompletePage() {
const navigate = useNavigate();
const { signInWithPassword } = useAuth();
const initialState = useMemo(() => getOnboardingSessionState(), []);
const [requestId] = useState<string | null>(initialState.requestId);
const [verifiedEmail] = useState<string | null>(initialState.verifiedEmail);
const [onboardingEmail] = useState<string | null>(initialState.onboardingEmail);
const [plans, setPlans] = useState<PlanSummary[]>([]);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const form = useForm<CompleteValues>({
resolver: zodResolver(onboardingCompleteSchema.omit({ onboardingRequestId: true })),
mode: 'onChange',
defaultValues: initialState.completeForm,
});
const values = useWatch({ control: form.control });
useEffect(() => {
if (!requestId) {
void navigate({ to: '/onboard' });
return;
}
if (!verifiedEmail) {
void navigate({ to: '/onboard/verify' });
}
}, [navigate, requestId, verifiedEmail]);
useEffect(() => {
updateOnboardingSessionState((current) => ({
...current,
completeForm: {
password: values.password ?? '',
complexName: values.complexName ?? '',
physicalAddress: values.physicalAddress ?? '',
planCode: values.planCode ?? '',
},
}));
}, [values.complexName, values.password, values.physicalAddress, values.planCode]);
useEffect(() => {
void (async () => {
try {
const result = await apiClient.plans.list();
setPlans(result);
} catch {
setPlans([]);
}
})();
}, []);
const onSubmit = async (payload: CompleteValues) => {
if (!requestId) {
setErrorMessage('No encontramos una solicitud de onboarding activa.');
return;
}
setErrorMessage(null);
try {
const result = await apiClient.onboarding.complete({
onboardingRequestId: requestId,
password: payload.password,
complexName: payload.complexName,
physicalAddress: payload.physicalAddress,
planCode: payload.planCode,
});
setCurrentComplexSlug(result.complexSlug);
const emailForSignIn = verifiedEmail ?? onboardingEmail;
if (emailForSignIn) {
await signInWithPassword({ email: emailForSignIn, password: payload.password });
}
clearOnboardingSessionState();
await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } });
} catch {
setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.');
}
};
const planPlaceholder =
plans.length === 0 ? 'Codigo del plan (por ejemplo: BASIC)' : 'Selecciona un plan';
return (
<OnboardingLayout
title="Completa tu onboarding"
description="Define tu contraseña y configura tu complejo."
>
<OnboardingCompleteStep
form={form}
plans={plans}
verifiedEmail={verifiedEmail}
errorMessage={errorMessage}
infoMessage={null}
planPlaceholder={planPlaceholder}
onSubmit={onSubmit}
/>
</OnboardingLayout>
);
}

View File

@@ -1,365 +0,0 @@
import { OnboardingCompleteStep } from '@/features/onboard/components/onboarding-complete-step';
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
import { OnboardingStartStep } from '@/features/onboard/components/onboarding-start-step';
import { OnboardingVerifyOtpStep } from '@/features/onboard/components/onboarding-verify-otp-step';
import type {
CompleteValues,
StartValues,
VerifyOtpValues,
} from '@/features/onboard/onboarding.types';
import { apiClient } from '@/lib/api-client';
import { useAuth } from '@/lib/auth';
import { setCurrentComplexSlug } from '@/lib/current-complex';
import { zodResolver } from '@hookform/resolvers/zod';
import {
type PlanSummary,
onboardingCompleteSchema,
onboardingStartSchema,
onboardingVerifyOtpSchema,
} from '@repo/api-contract';
import { useNavigate } from '@tanstack/react-router';
import { useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
const OTP_MAX_ATTEMPTS = 5;
type OnboardStep = 'start' | 'verify-otp' | 'complete';
const ONBOARD_SESSION_KEY = 'playzer:onboarding-session';
type OnboardSessionState = {
step: OnboardStep;
requestId: string | null;
onboardingEmail: string | null;
verifiedEmail: string | null;
remainingAttempts: number;
resendCooldownSeconds: number;
startForm: StartValues;
verifyOtpForm: VerifyOtpValues;
completeForm: CompleteValues;
};
function isOnboardStep(value: unknown): value is OnboardStep {
return value === 'start' || value === 'verify-otp' || value === 'complete';
}
function readOnboardSession(): OnboardSessionState | null {
if (typeof window === 'undefined') return null;
const raw = window.sessionStorage.getItem(ONBOARD_SESSION_KEY);
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as Partial<OnboardSessionState>;
if (!isOnboardStep(parsed.step)) return null;
return {
step: parsed.step,
requestId: typeof parsed.requestId === 'string' ? parsed.requestId : null,
onboardingEmail: typeof parsed.onboardingEmail === 'string' ? parsed.onboardingEmail : null,
verifiedEmail: typeof parsed.verifiedEmail === 'string' ? parsed.verifiedEmail : null,
remainingAttempts:
typeof parsed.remainingAttempts === 'number'
? parsed.remainingAttempts
: OTP_MAX_ATTEMPTS,
resendCooldownSeconds:
typeof parsed.resendCooldownSeconds === 'number' ? parsed.resendCooldownSeconds : 0,
startForm: {
fullName: parsed.startForm?.fullName ?? '',
email: parsed.startForm?.email ?? '',
},
verifyOtpForm: {
otp: parsed.verifyOtpForm?.otp ?? '',
},
completeForm: {
password: parsed.completeForm?.password ?? '',
complexName: parsed.completeForm?.complexName ?? '',
physicalAddress: parsed.completeForm?.physicalAddress ?? '',
planCode: parsed.completeForm?.planCode ?? '',
},
};
} catch {
return null;
}
}
function clearOnboardSession() {
if (typeof window === 'undefined') return;
window.sessionStorage.removeItem(ONBOARD_SESSION_KEY);
}
export function OnboardPage() {
const navigate = useNavigate();
const { signInWithPassword } = useAuth();
const [step, setStep] = useState<OnboardStep>('start');
const [requestId, setRequestId] = useState<string | null>(null);
const [onboardingEmail, setOnboardingEmail] = useState<string | null>(null);
const [verifiedEmail, setVerifiedEmail] = useState<string | null>(null);
const [plans, setPlans] = useState<PlanSummary[]>([]);
const [remainingAttempts, setRemainingAttempts] = useState<number>(OTP_MAX_ATTEMPTS);
const [resendCooldownSeconds, setResendCooldownSeconds] = useState<number>(0);
const [isResending, setIsResending] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [infoMessage, setInfoMessage] = useState<string | null>(null);
const [isSessionRestored, setIsSessionRestored] = useState(false);
const startForm = useForm<StartValues>({
resolver: zodResolver(onboardingStartSchema),
mode: 'onChange',
defaultValues: {
fullName: '',
email: '',
},
});
const completeForm = useForm<CompleteValues>({
resolver: zodResolver(onboardingCompleteSchema.omit({ onboardingRequestId: true })),
mode: 'onChange',
defaultValues: {
password: '',
complexName: '',
physicalAddress: '',
planCode: '',
},
});
const verifyOtpForm = useForm<VerifyOtpValues>({
resolver: zodResolver(onboardingVerifyOtpSchema.omit({ requestId: true })),
mode: 'onChange',
defaultValues: {
otp: '',
},
});
const startValues = useWatch({ control: startForm.control });
const verifyOtpValues = useWatch({ control: verifyOtpForm.control });
const completeValues = useWatch({ control: completeForm.control });
useEffect(() => {
const savedSession = readOnboardSession();
if (!savedSession) {
setIsSessionRestored(true);
return;
}
setStep(savedSession.step);
setRequestId(savedSession.requestId);
setOnboardingEmail(savedSession.onboardingEmail);
setVerifiedEmail(savedSession.verifiedEmail);
setRemainingAttempts(savedSession.remainingAttempts);
setResendCooldownSeconds(savedSession.resendCooldownSeconds);
startForm.reset(savedSession.startForm);
verifyOtpForm.reset(savedSession.verifyOtpForm);
completeForm.reset(savedSession.completeForm);
setIsSessionRestored(true);
}, [startForm, verifyOtpForm, completeForm]);
useEffect(() => {
if (!isSessionRestored || typeof window === 'undefined') return;
const sessionState: OnboardSessionState = {
step,
requestId,
onboardingEmail,
verifiedEmail,
remainingAttempts,
resendCooldownSeconds,
startForm: {
fullName: startValues.fullName ?? '',
email: startValues.email ?? '',
},
verifyOtpForm: {
otp: verifyOtpValues.otp ?? '',
},
completeForm: {
password: completeValues.password ?? '',
complexName: completeValues.complexName ?? '',
physicalAddress: completeValues.physicalAddress ?? '',
planCode: completeValues.planCode ?? '',
},
};
window.sessionStorage.setItem(ONBOARD_SESSION_KEY, JSON.stringify(sessionState));
}, [
completeValues,
isSessionRestored,
onboardingEmail,
remainingAttempts,
requestId,
resendCooldownSeconds,
startValues,
step,
verifiedEmail,
verifyOtpValues,
]);
useEffect(() => {
if (step !== 'complete') return;
void (async () => {
try {
const result = await apiClient.plans.list();
setPlans(result);
} catch {
setPlans([]);
}
})();
}, [step]);
const planPlaceholder = useMemo(() => {
if (plans.length === 0) return 'Codigo del plan (por ejemplo: BASIC)';
return 'Selecciona un plan';
}, [plans.length]);
useEffect(() => {
if (resendCooldownSeconds <= 0) return;
const timeout = window.setTimeout(() => {
setResendCooldownSeconds((current) => Math.max(0, current - 1));
}, 1000);
return () => {
window.clearTimeout(timeout);
};
}, [resendCooldownSeconds]);
const onSubmitStart = async (values: StartValues) => {
setErrorMessage(null);
setInfoMessage(null);
try {
const result = await apiClient.onboarding.start(values);
setRequestId(result.requestId);
setOnboardingEmail(result.email);
setRemainingAttempts(OTP_MAX_ATTEMPTS);
setResendCooldownSeconds(result.cooldownSeconds);
verifyOtpForm.reset({ otp: '' });
setStep('verify-otp');
setInfoMessage(result.message);
} catch {
setErrorMessage('No pudimos iniciar el onboarding. Intenta nuevamente.');
}
};
const onSubmitVerifyOtp = async (values: VerifyOtpValues) => {
if (!requestId) {
setErrorMessage('No encontramos una solicitud de onboarding activa.');
return;
}
setErrorMessage(null);
try {
const result = await apiClient.onboarding.verifyOtp({
requestId,
otp: values.otp,
});
setRemainingAttempts(result.remainingAttempts);
setResendCooldownSeconds(result.cooldownSeconds);
setInfoMessage(result.message);
if (result.verified) {
setVerifiedEmail(result.email ?? onboardingEmail);
setStep('complete');
return;
}
} catch {
setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.');
}
};
const onResendOtp = async () => {
if (!requestId) {
setErrorMessage('No encontramos una solicitud de onboarding activa.');
return;
}
setIsResending(true);
setErrorMessage(null);
try {
const result = await apiClient.onboarding.resendOtp({ requestId });
setOnboardingEmail(result.email);
setRemainingAttempts(result.remainingAttempts);
setResendCooldownSeconds(result.cooldownSeconds);
verifyOtpForm.reset({ otp: '' });
setInfoMessage(result.message);
} catch {
setErrorMessage('No pudimos reenviar el OTP. Intenta nuevamente.');
} finally {
setIsResending(false);
}
};
const onSubmitComplete = async (values: CompleteValues) => {
if (!requestId) {
setErrorMessage('No encontramos una solicitud de onboarding activa.');
return;
}
setErrorMessage(null);
try {
const result = await apiClient.onboarding.complete({
onboardingRequestId: requestId,
password: values.password,
complexName: values.complexName,
physicalAddress: values.physicalAddress,
planCode: values.planCode,
});
setCurrentComplexSlug(result.complexSlug);
const emailForSignIn = verifiedEmail ?? onboardingEmail;
if (emailForSignIn) {
await signInWithPassword({ email: emailForSignIn, password: values.password });
}
clearOnboardSession();
await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } });
} catch {
setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.');
}
};
return (
<OnboardingLayout
title="Onboarding"
description="Crea tu cuenta y configura tu complejo de canchas."
>
{step === 'start' && (
<OnboardingStartStep
form={startForm}
errorMessage={errorMessage}
infoMessage={infoMessage}
onSubmit={onSubmitStart}
/>
)}
{step === 'verify-otp' && (
<OnboardingVerifyOtpStep
form={verifyOtpForm}
email={onboardingEmail}
errorMessage={errorMessage}
infoMessage={infoMessage}
remainingAttempts={remainingAttempts}
resendCooldownSeconds={resendCooldownSeconds}
isResending={isResending}
onSubmit={onSubmitVerifyOtp}
onResend={onResendOtp}
/>
)}
{step === 'complete' && (
<OnboardingCompleteStep
form={completeForm}
plans={plans}
verifiedEmail={verifiedEmail}
errorMessage={errorMessage}
infoMessage={infoMessage}
planPlaceholder={planPlaceholder}
onSubmit={onSubmitComplete}
/>
)}
</OnboardingLayout>
);
}

View File

@@ -0,0 +1,79 @@
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
import { OnboardingStartStep } from '@/features/onboard/components/onboarding-start-step';
import {
OTP_MAX_ATTEMPTS,
defaultCompleteFormValues,
defaultVerifyOtpFormValues,
getOnboardingSessionState,
updateOnboardingSessionState,
} from '@/features/onboard/onboarding-session';
import type { StartValues } from '@/features/onboard/onboarding.types';
import { apiClient } from '@/lib/api-client';
import { zodResolver } from '@hookform/resolvers/zod';
import { onboardingStartSchema } from '@repo/api-contract';
import { useNavigate } from '@tanstack/react-router';
import { useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
export function OnboardStartPage() {
const navigate = useNavigate();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [infoMessage, setInfoMessage] = useState<string | null>(null);
const initialState = useMemo(() => getOnboardingSessionState(), []);
const form = useForm<StartValues>({
resolver: zodResolver(onboardingStartSchema),
mode: 'onChange',
defaultValues: initialState.startForm,
});
const values = useWatch({ control: form.control });
useEffect(() => {
updateOnboardingSessionState((current) => ({
...current,
startForm: {
fullName: values.fullName ?? '',
email: values.email ?? '',
},
}));
}, [values.email, values.fullName]);
const onSubmit = async (payload: StartValues) => {
setErrorMessage(null);
setInfoMessage(null);
try {
const result = await apiClient.onboarding.start(payload);
updateOnboardingSessionState((current) => ({
...current,
requestId: result.requestId,
onboardingEmail: result.email,
verifiedEmail: null,
remainingAttempts: OTP_MAX_ATTEMPTS,
resendCooldownSeconds: result.cooldownSeconds,
startForm: {
fullName: payload.fullName,
email: payload.email,
},
verifyOtpForm: defaultVerifyOtpFormValues,
completeForm: defaultCompleteFormValues,
}));
await navigate({ to: '/onboard/verify' });
} catch {
setErrorMessage('No pudimos iniciar el onboarding. Intenta nuevamente.');
}
};
return (
<OnboardingLayout title="Onboarding" description="Ingresa tu nombre y email para comenzar.">
<OnboardingStartStep
form={form}
errorMessage={errorMessage}
infoMessage={infoMessage}
onSubmit={onSubmit}
/>
</OnboardingLayout>
);
}

View File

@@ -0,0 +1,169 @@
import { Button } from '@/components/ui/button';
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
import { OnboardingVerifyOtpStep } from '@/features/onboard/components/onboarding-verify-otp-step';
import {
OTP_MAX_ATTEMPTS,
getOnboardingSessionState,
updateOnboardingSessionState,
} from '@/features/onboard/onboarding-session';
import type { VerifyOtpValues } from '@/features/onboard/onboarding.types';
import { apiClient } from '@/lib/api-client';
import { zodResolver } from '@hookform/resolvers/zod';
import { onboardingVerifyOtpSchema } from '@repo/api-contract';
import { useNavigate } from '@tanstack/react-router';
import { useEffect, useMemo, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
export function OnboardVerifyPage() {
const navigate = useNavigate();
const initialState = useMemo(() => getOnboardingSessionState(), []);
const [requestId] = useState<string | null>(initialState.requestId);
const [onboardingEmail, setOnboardingEmail] = useState<string | null>(
initialState.onboardingEmail
);
const [remainingAttempts, setRemainingAttempts] = useState<number>(
initialState.remainingAttempts || OTP_MAX_ATTEMPTS
);
const [resendCooldownSeconds, setResendCooldownSeconds] = useState<number>(
initialState.resendCooldownSeconds || 0
);
const [isResending, setIsResending] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [infoMessage, setInfoMessage] = useState<string | null>(null);
const form = useForm<VerifyOtpValues>({
resolver: zodResolver(onboardingVerifyOtpSchema.omit({ requestId: true })),
mode: 'onChange',
defaultValues: initialState.verifyOtpForm,
});
const values = useWatch({ control: form.control });
useEffect(() => {
if (requestId && onboardingEmail) return;
void navigate({ to: '/onboard' });
}, [navigate, onboardingEmail, requestId]);
useEffect(() => {
updateOnboardingSessionState((current) => ({
...current,
onboardingEmail,
remainingAttempts,
resendCooldownSeconds,
verifyOtpForm: {
otp: values.otp ?? '',
},
}));
}, [onboardingEmail, remainingAttempts, resendCooldownSeconds, values.otp]);
useEffect(() => {
if (resendCooldownSeconds <= 0) return;
const timeout = window.setTimeout(() => {
setResendCooldownSeconds((current) => Math.max(0, current - 1));
}, 1000);
return () => {
window.clearTimeout(timeout);
};
}, [resendCooldownSeconds]);
const onSubmit = async (payload: VerifyOtpValues) => {
if (!requestId) {
setErrorMessage('No encontramos una solicitud de onboarding activa.');
return;
}
setErrorMessage(null);
try {
const result = await apiClient.onboarding.verifyOtp({
requestId,
otp: payload.otp,
});
setRemainingAttempts(result.remainingAttempts);
setResendCooldownSeconds(result.cooldownSeconds);
setInfoMessage(result.message);
updateOnboardingSessionState((current) => ({
...current,
remainingAttempts: result.remainingAttempts,
resendCooldownSeconds: result.cooldownSeconds,
}));
if (result.verified) {
const nextVerifiedEmail = result.email ?? onboardingEmail;
updateOnboardingSessionState((current) => ({
...current,
verifiedEmail: nextVerifiedEmail,
remainingAttempts: result.remainingAttempts,
resendCooldownSeconds: result.cooldownSeconds,
}));
await navigate({ to: '/onboard/complete' });
}
} catch {
setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.');
}
};
const onResend = async () => {
if (!requestId) {
setErrorMessage('No encontramos una solicitud de onboarding activa.');
return;
}
setIsResending(true);
setErrorMessage(null);
try {
const result = await apiClient.onboarding.resendOtp({ requestId });
setOnboardingEmail(result.email);
setRemainingAttempts(result.remainingAttempts);
setResendCooldownSeconds(result.cooldownSeconds);
form.reset({ otp: '' });
setInfoMessage(result.message);
updateOnboardingSessionState((current) => ({
...current,
onboardingEmail: result.email,
remainingAttempts: result.remainingAttempts,
resendCooldownSeconds: result.cooldownSeconds,
verifyOtpForm: { otp: '' },
}));
} catch {
setErrorMessage('No pudimos reenviar el OTP. Intenta nuevamente.');
} finally {
setIsResending(false);
}
};
return (
<OnboardingLayout
title="Verifica tu email"
description="Ingresa el OTP para validar tu cuenta."
>
<OnboardingVerifyOtpStep
form={form}
email={onboardingEmail}
errorMessage={errorMessage}
infoMessage={infoMessage}
remainingAttempts={remainingAttempts}
resendCooldownSeconds={resendCooldownSeconds}
isResending={isResending}
onSubmit={onSubmit}
onResend={onResend}
/>
<Button
variant="ghost"
className="mt-2 w-full"
onClick={() => {
void navigate({ to: '/onboard' });
}}
>
Volver al paso anterior
</Button>
</OnboardingLayout>
);
}

View File

@@ -0,0 +1,140 @@
import type {
CompleteValues,
StartValues,
VerifyOtpValues,
} from '@/features/onboard/onboarding.types';
const ONBOARDING_SESSION_KEY = 'playzer:onboarding-session';
export const OTP_MAX_ATTEMPTS = 5;
export const defaultStartFormValues: StartValues = {
fullName: '',
email: '',
};
export const defaultVerifyOtpFormValues: VerifyOtpValues = {
otp: '',
};
export const defaultCompleteFormValues: CompleteValues = {
password: '',
complexName: '',
physicalAddress: '',
planCode: '',
};
export type OnboardingSessionState = {
requestId: string | null;
onboardingEmail: string | null;
verifiedEmail: string | null;
remainingAttempts: number;
resendCooldownSeconds: number;
startForm: StartValues;
verifyOtpForm: VerifyOtpValues;
completeForm: CompleteValues;
};
const defaultOnboardingSessionState: OnboardingSessionState = {
requestId: null,
onboardingEmail: null,
verifiedEmail: null,
remainingAttempts: OTP_MAX_ATTEMPTS,
resendCooldownSeconds: 0,
startForm: defaultStartFormValues,
verifyOtpForm: defaultVerifyOtpFormValues,
completeForm: defaultCompleteFormValues,
};
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function normalizeSessionState(value: unknown): OnboardingSessionState {
if (!isObject(value)) {
return defaultOnboardingSessionState;
}
return {
requestId: typeof value.requestId === 'string' ? value.requestId : null,
onboardingEmail: typeof value.onboardingEmail === 'string' ? value.onboardingEmail : null,
verifiedEmail: typeof value.verifiedEmail === 'string' ? value.verifiedEmail : null,
remainingAttempts:
typeof value.remainingAttempts === 'number'
? value.remainingAttempts
: defaultOnboardingSessionState.remainingAttempts,
resendCooldownSeconds:
typeof value.resendCooldownSeconds === 'number'
? value.resendCooldownSeconds
: defaultOnboardingSessionState.resendCooldownSeconds,
startForm: {
fullName:
isObject(value.startForm) && typeof value.startForm.fullName === 'string'
? value.startForm.fullName
: defaultStartFormValues.fullName,
email:
isObject(value.startForm) && typeof value.startForm.email === 'string'
? value.startForm.email
: defaultStartFormValues.email,
},
verifyOtpForm: {
otp:
isObject(value.verifyOtpForm) && typeof value.verifyOtpForm.otp === 'string'
? value.verifyOtpForm.otp
: defaultVerifyOtpFormValues.otp,
},
completeForm: {
password:
isObject(value.completeForm) && typeof value.completeForm.password === 'string'
? value.completeForm.password
: defaultCompleteFormValues.password,
complexName:
isObject(value.completeForm) && typeof value.completeForm.complexName === 'string'
? value.completeForm.complexName
: defaultCompleteFormValues.complexName,
physicalAddress:
isObject(value.completeForm) && typeof value.completeForm.physicalAddress === 'string'
? value.completeForm.physicalAddress
: defaultCompleteFormValues.physicalAddress,
planCode:
isObject(value.completeForm) && typeof value.completeForm.planCode === 'string'
? value.completeForm.planCode
: defaultCompleteFormValues.planCode,
},
};
}
export function getOnboardingSessionState(): OnboardingSessionState {
if (typeof window === 'undefined') {
return defaultOnboardingSessionState;
}
const rawState = window.sessionStorage.getItem(ONBOARDING_SESSION_KEY);
if (!rawState) {
return defaultOnboardingSessionState;
}
try {
return normalizeSessionState(JSON.parse(rawState));
} catch {
return defaultOnboardingSessionState;
}
}
export function setOnboardingSessionState(state: OnboardingSessionState) {
if (typeof window === 'undefined') return;
window.sessionStorage.setItem(ONBOARDING_SESSION_KEY, JSON.stringify(state));
}
export function updateOnboardingSessionState(
updater: (current: OnboardingSessionState) => OnboardingSessionState
): OnboardingSessionState {
const currentState = getOnboardingSessionState();
const nextState = updater(currentState);
setOnboardingSessionState(nextState);
return nextState;
}
export function clearOnboardingSessionState() {
if (typeof window === 'undefined') return;
window.sessionStorage.removeItem(ONBOARDING_SESSION_KEY);
}

View File

@@ -52,9 +52,9 @@ function md5(string: string) {
let c = 0x98badcfe;
let d = 0x10325476;
const s = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,
20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10,
15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];
const str = unescape(encodeURIComponent(string));
@@ -69,7 +69,7 @@ function md5(string: string) {
words[wordLen * 16 - 2] = len * 8;
for (let i = 0; i < words.length; i += 16) {
let [aa, bb, cc, dd] = [a, b, c, d];
const [aa, bb, cc, dd] = [a, b, c, d];
for (let j = 0; j < 64; j++) {
let f: number;
let g: number;
@@ -91,7 +91,8 @@ function md5(string: string) {
c = b;
b =
(b +
((a + f + k[j] + words[i + g]) << s[j] | (a + f + k[j] + words[i + g]) >>> (32 - s[j]))) |
(((a + f + k[j] + words[i + g]) << s[j]) |
((a + f + k[j] + words[i + g]) >>> (32 - s[j])))) |
0;
a = temp;
}
@@ -103,9 +104,9 @@ function md5(string: string) {
return [a, b, c, d]
.map((v) =>
Array.from({ length: 4 }, (_, i) => ((v >> (i * 8)) & 0xff).toString(16).padStart(2, '0')).join(
''
)
Array.from({ length: 4 }, (_, i) =>
((v >> (i * 8)) & 0xff).toString(16).padStart(2, '0')
).join('')
)
.join('');
}

View File

@@ -13,6 +13,9 @@ import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as OnboardRouteImport } from './routes/onboard'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AppRouteRouteImport } from './routes/_app/route'
import { Route as OnboardIndexRouteImport } from './routes/onboard/index'
import { Route as OnboardVerifyRouteImport } from './routes/onboard/verify'
import { Route as OnboardCompleteRouteImport } from './routes/onboard/complete'
import { Route as AppProfileRouteImport } from './routes/_app/profile'
import { Route as AppAboutRouteImport } from './routes/_app/about'
import { Route as ComplexSlugBookingRouteImport } from './routes/$complexSlug/booking'
@@ -41,6 +44,21 @@ const AppRouteRoute = AppRouteRouteImport.update({
id: '/_app',
getParentRoute: () => rootRouteImport,
} as any)
const OnboardIndexRoute = OnboardIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => OnboardRoute,
} as any)
const OnboardVerifyRoute = OnboardVerifyRouteImport.update({
id: '/verify',
path: '/verify',
getParentRoute: () => OnboardRoute,
} as any)
const OnboardCompleteRoute = OnboardCompleteRouteImport.update({
id: '/complete',
path: '/complete',
getParentRoute: () => OnboardRoute,
} as any)
const AppProfileRoute = AppProfileRouteImport.update({
id: '/profile',
path: '/profile',
@@ -86,11 +104,14 @@ const AppAuthenticatedComplexSlugEditRoute =
export interface FileRoutesByFullPath {
'/': typeof AppAuthenticatedIndexRoute
'/login': typeof LoginRoute
'/onboard': typeof OnboardRoute
'/onboard': typeof OnboardRouteWithChildren
'/reset-password': typeof ResetPasswordRoute
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
'/about': typeof AppAboutRoute
'/profile': typeof AppProfileRoute
'/onboard/complete': typeof OnboardCompleteRoute
'/onboard/verify': typeof OnboardVerifyRoute
'/onboard/': typeof OnboardIndexRoute
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
@@ -98,10 +119,12 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof AppAuthenticatedIndexRoute
'/login': typeof LoginRoute
'/onboard': typeof OnboardRoute
'/reset-password': typeof ResetPasswordRoute
'/about': typeof AppAboutRoute
'/profile': typeof AppProfileRoute
'/onboard/complete': typeof OnboardCompleteRoute
'/onboard/verify': typeof OnboardVerifyRoute
'/onboard': typeof OnboardIndexRoute
'/$complexSlug/booking': typeof ComplexSlugBookingIndexRoute
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
@@ -110,12 +133,15 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_app': typeof AppRouteRouteWithChildren
'/login': typeof LoginRoute
'/onboard': typeof OnboardRoute
'/onboard': typeof OnboardRouteWithChildren
'/reset-password': typeof ResetPasswordRoute
'/_app/_authenticated': typeof AppAuthenticatedRouteRouteWithChildren
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
'/_app/about': typeof AppAboutRoute
'/_app/profile': typeof AppProfileRoute
'/onboard/complete': typeof OnboardCompleteRoute
'/onboard/verify': typeof OnboardVerifyRoute
'/onboard/': typeof OnboardIndexRoute
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
'/_app/_authenticated/': typeof AppAuthenticatedIndexRoute
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
@@ -131,6 +157,9 @@ export interface FileRouteTypes {
| '/$complexSlug/booking'
| '/about'
| '/profile'
| '/onboard/complete'
| '/onboard/verify'
| '/onboard/'
| '/$complexSlug/booking/'
| '/$complexSlug/booking/confirmed/$bookingCode'
| '/complex/$slug/edit'
@@ -138,10 +167,12 @@ export interface FileRouteTypes {
to:
| '/'
| '/login'
| '/onboard'
| '/reset-password'
| '/about'
| '/profile'
| '/onboard/complete'
| '/onboard/verify'
| '/onboard'
| '/$complexSlug/booking'
| '/$complexSlug/booking/confirmed/$bookingCode'
| '/complex/$slug/edit'
@@ -155,6 +186,9 @@ export interface FileRouteTypes {
| '/$complexSlug/booking'
| '/_app/about'
| '/_app/profile'
| '/onboard/complete'
| '/onboard/verify'
| '/onboard/'
| '/$complexSlug/booking/'
| '/_app/_authenticated/'
| '/$complexSlug/booking/confirmed/$bookingCode'
@@ -164,7 +198,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
AppRouteRoute: typeof AppRouteRouteWithChildren
LoginRoute: typeof LoginRoute
OnboardRoute: typeof OnboardRoute
OnboardRoute: typeof OnboardRouteWithChildren
ResetPasswordRoute: typeof ResetPasswordRoute
ComplexSlugBookingRoute: typeof ComplexSlugBookingRouteWithChildren
}
@@ -199,6 +233,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/onboard/': {
id: '/onboard/'
path: '/'
fullPath: '/onboard/'
preLoaderRoute: typeof OnboardIndexRouteImport
parentRoute: typeof OnboardRoute
}
'/onboard/verify': {
id: '/onboard/verify'
path: '/verify'
fullPath: '/onboard/verify'
preLoaderRoute: typeof OnboardVerifyRouteImport
parentRoute: typeof OnboardRoute
}
'/onboard/complete': {
id: '/onboard/complete'
path: '/complete'
fullPath: '/onboard/complete'
preLoaderRoute: typeof OnboardCompleteRouteImport
parentRoute: typeof OnboardRoute
}
'/_app/profile': {
id: '/_app/profile'
path: '/profile'
@@ -289,6 +344,21 @@ const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
AppRouteRouteChildren,
)
interface OnboardRouteChildren {
OnboardCompleteRoute: typeof OnboardCompleteRoute
OnboardVerifyRoute: typeof OnboardVerifyRoute
OnboardIndexRoute: typeof OnboardIndexRoute
}
const OnboardRouteChildren: OnboardRouteChildren = {
OnboardCompleteRoute: OnboardCompleteRoute,
OnboardVerifyRoute: OnboardVerifyRoute,
OnboardIndexRoute: OnboardIndexRoute,
}
const OnboardRouteWithChildren =
OnboardRoute._addFileChildren(OnboardRouteChildren)
interface ComplexSlugBookingRouteChildren {
ComplexSlugBookingIndexRoute: typeof ComplexSlugBookingIndexRoute
ComplexSlugBookingConfirmedBookingCodeRoute: typeof ComplexSlugBookingConfirmedBookingCodeRoute
@@ -306,7 +376,7 @@ const ComplexSlugBookingRouteWithChildren =
const rootRouteChildren: RootRouteChildren = {
AppRouteRoute: AppRouteRouteWithChildren,
LoginRoute: LoginRoute,
OnboardRoute: OnboardRoute,
OnboardRoute: OnboardRouteWithChildren,
ResetPasswordRoute: ResetPasswordRoute,
ComplexSlugBookingRoute: ComplexSlugBookingRouteWithChildren,
}

View File

@@ -1,6 +1,5 @@
import { OnboardPage } from '@/features/onboard/onboard-page';
import { createFileRoute } from '@tanstack/react-router';
import { Outlet, createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/onboard')({
component: OnboardPage,
component: Outlet,
});

View File

@@ -0,0 +1,6 @@
import { OnboardCompletePage } from '@/features/onboard/onboard-complete-page';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/onboard/complete')({
component: OnboardCompletePage,
});

View File

@@ -0,0 +1,6 @@
import { OnboardStartPage } from '@/features/onboard/onboard-start-page';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/onboard/')({
component: OnboardStartPage,
});

View File

@@ -0,0 +1,6 @@
import { OnboardVerifyPage } from '@/features/onboard/onboard-verify-page';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/onboard/verify')({
component: OnboardVerifyPage,
});