feat: refactor onboarding into a multi-step flow with dedicated routes and session state management
This commit is contained in:
119
apps/frontend/src/features/onboard/onboard-complete-page.tsx
Normal file
119
apps/frontend/src/features/onboard/onboard-complete-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
79
apps/frontend/src/features/onboard/onboard-start-page.tsx
Normal file
79
apps/frontend/src/features/onboard/onboard-start-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
169
apps/frontend/src/features/onboard/onboard-verify-page.tsx
Normal file
169
apps/frontend/src/features/onboard/onboard-verify-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
140
apps/frontend/src/features/onboard/onboarding-session.ts
Normal file
140
apps/frontend/src/features/onboard/onboarding-session.ts
Normal 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);
|
||||
}
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
6
apps/frontend/src/routes/onboard/complete.tsx
Normal file
6
apps/frontend/src/routes/onboard/complete.tsx
Normal 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,
|
||||
});
|
||||
6
apps/frontend/src/routes/onboard/index.tsx
Normal file
6
apps/frontend/src/routes/onboard/index.tsx
Normal 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,
|
||||
});
|
||||
6
apps/frontend/src/routes/onboard/verify.tsx
Normal file
6
apps/frontend/src/routes/onboard/verify.tsx
Normal 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,
|
||||
});
|
||||
Reference in New Issue
Block a user