Merge pull request 'feat: implement session persistence for onboarding flow and fix typo in password label' (#3) from fix-onboarding into development

Reviewed-on: https://gitea.2pidev.com/jselesan/playzer/pulls/3
This commit is contained in:
2026-04-17 19:04:59 +00:00
3 changed files with 130 additions and 3 deletions

View File

@@ -13,4 +13,5 @@ SMTP_FROM=Playzer <no-reply@playzer.app>
BETTER_AUTH_SECRET=R1MfeeeekXSNdE65hOhhr0Mt0KLwczYr
BETTER_AUTH_URL=http://localhost:3000
MAILTRAP_API_TOKEN=2c040e2e577dc48909d3206dd2d47fc6
MAIL_SANDBOX=true
MAIL_SANDBOX=true
MAIL_INBOX_ID=1114587

View File

@@ -34,7 +34,7 @@ export function OnboardingCompleteStep({
)}
<Field data-invalid={Boolean(form.formState.errors.password)}>
<FieldLabel htmlFor="onboard-password">Contrasena</FieldLabel>
<FieldLabel htmlFor="onboard-password">Contraseña</FieldLabel>
<Input
id="onboard-password"
type="password"

View File

@@ -19,10 +19,72 @@ import {
} from '@repo/api-contract';
import { useNavigate } from '@tanstack/react-router';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
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();
@@ -38,6 +100,7 @@ export function OnboardPage() {
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),
@@ -67,6 +130,68 @@ export function OnboardPage() {
},
});
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;
@@ -189,6 +314,7 @@ export function OnboardPage() {
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.');