feat: add city/state/country to complex, new settings page with sidebar, and Biome linting
- Added city, state, and country optional fields to Complex model - Updated onboarding to include optional location fields - Created new settings page with sidebar navigation (Datos del Complejo, Canchas) - Replaced ESLint with Biome for frontend and backend linting - Added parallel dev script with concurrently - Migrated register-routes to use direct app.route() pattern
This commit is contained in:
@@ -3,5 +3,5 @@ export function OnboardingCheckEmailStep() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Revisa tu correo y abre el link de verificacion para continuar.
|
||||
</p>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { PlanSummary } from '@repo/api-contract'
|
||||
import { Controller, type UseFormReturn } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import type { CompleteValues } from '@/features/onboard/onboarding.types'
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { CompleteValues } from '@/features/onboard/onboarding.types';
|
||||
import type { PlanSummary } from '@repo/api-contract';
|
||||
import { Controller, type UseFormReturn } from 'react-hook-form';
|
||||
|
||||
type OnboardingCompleteStepProps = {
|
||||
form: UseFormReturn<CompleteValues>
|
||||
plans: PlanSummary[]
|
||||
verifiedEmail: string | null
|
||||
errorMessage: string | null
|
||||
infoMessage: string | null
|
||||
planPlaceholder: string
|
||||
onSubmit: (values: CompleteValues) => Promise<void>
|
||||
}
|
||||
form: UseFormReturn<CompleteValues>;
|
||||
plans: PlanSummary[];
|
||||
verifiedEmail: string | null;
|
||||
errorMessage: string | null;
|
||||
infoMessage: string | null;
|
||||
planPlaceholder: string;
|
||||
onSubmit: (values: CompleteValues) => Promise<void>;
|
||||
};
|
||||
|
||||
export function OnboardingCompleteStep({
|
||||
form,
|
||||
@@ -81,7 +81,7 @@ export function OnboardingCompleteStep({
|
||||
aria-invalid={Boolean(form.formState.errors.planCode)}
|
||||
value={field.value ?? ''}
|
||||
onChange={(event) => {
|
||||
field.onChange(event.target.value)
|
||||
field.onChange(event.target.value);
|
||||
}}
|
||||
disabled={plans.length === 0}
|
||||
>
|
||||
@@ -115,5 +115,5 @@ export function OnboardingCompleteStep({
|
||||
{form.formState.isSubmitting ? 'Finalizando...' : 'Completar onboarding'}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
type OnboardingLayoutProps = PropsWithChildren<{
|
||||
title: string
|
||||
description: string
|
||||
}>
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
|
||||
export function OnboardingLayout({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: OnboardingLayoutProps) {
|
||||
export function OnboardingLayout({ title, description, children }: OnboardingLayoutProps) {
|
||||
return (
|
||||
<main className="mx-auto flex min-h-screen w-full max-w-6xl items-center justify-center px-6">
|
||||
<section className="w-full max-w-3xl rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
|
||||
@@ -18,5 +14,5 @@ export function OnboardingLayout({
|
||||
{children}
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { UseFormReturn } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import type { StartValues } from '@/features/onboard/onboarding.types'
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { StartValues } from '@/features/onboard/onboarding.types';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
|
||||
type OnboardingStartStepProps = {
|
||||
form: UseFormReturn<StartValues>
|
||||
errorMessage: string | null
|
||||
infoMessage: string | null
|
||||
onSubmit: (values: StartValues) => Promise<void>
|
||||
}
|
||||
form: UseFormReturn<StartValues>;
|
||||
errorMessage: string | null;
|
||||
infoMessage: string | null;
|
||||
onSubmit: (values: StartValues) => Promise<void>;
|
||||
};
|
||||
|
||||
export function OnboardingStartStep({
|
||||
form,
|
||||
@@ -54,5 +54,5 @@ export function OnboardingStartStep({
|
||||
{form.formState.isSubmitting ? 'Enviando...' : 'Enviar codigo OTP'}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
import type { UseFormReturn } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSeparator,
|
||||
InputOTPSlot,
|
||||
} from '@/components/ui/input-otp'
|
||||
import type { VerifyOtpValues } from '@/features/onboard/onboarding.types'
|
||||
} from '@/components/ui/input-otp';
|
||||
import type { VerifyOtpValues } from '@/features/onboard/onboarding.types';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
|
||||
type OnboardingVerifyOtpStepProps = {
|
||||
form: UseFormReturn<VerifyOtpValues>
|
||||
email: string | null
|
||||
errorMessage: string | null
|
||||
infoMessage: string | null
|
||||
remainingAttempts: number
|
||||
resendCooldownSeconds: number
|
||||
isResending: boolean
|
||||
onSubmit: (values: VerifyOtpValues) => Promise<void>
|
||||
onResend: () => Promise<void>
|
||||
}
|
||||
form: UseFormReturn<VerifyOtpValues>;
|
||||
email: string | null;
|
||||
errorMessage: string | null;
|
||||
infoMessage: string | null;
|
||||
remainingAttempts: number;
|
||||
resendCooldownSeconds: number;
|
||||
isResending: boolean;
|
||||
onSubmit: (values: VerifyOtpValues) => Promise<void>;
|
||||
onResend: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function OnboardingVerifyOtpStep({
|
||||
form,
|
||||
@@ -33,7 +33,7 @@ export function OnboardingVerifyOtpStep({
|
||||
onSubmit,
|
||||
onResend,
|
||||
}: OnboardingVerifyOtpStepProps) {
|
||||
const resendDisabled = resendCooldownSeconds > 0 || isResending
|
||||
const resendDisabled = resendCooldownSeconds > 0 || isResending;
|
||||
|
||||
return (
|
||||
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
@@ -51,7 +51,7 @@ export function OnboardingVerifyOtpStep({
|
||||
size="sm"
|
||||
disabled={resendDisabled}
|
||||
onClick={() => {
|
||||
void onResend()
|
||||
void onResend();
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
@@ -70,7 +70,7 @@ export function OnboardingVerifyOtpStep({
|
||||
pattern="\d*"
|
||||
value={form.watch('otp')}
|
||||
onChange={(value) => {
|
||||
form.setValue('otp', value, { shouldValidate: true })
|
||||
form.setValue('otp', value, { shouldValidate: true });
|
||||
}}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
@@ -104,5 +104,5 @@ export function OnboardingVerifyOtpStep({
|
||||
{form.formState.isSubmitting ? 'Verificando...' : 'Verificar codigo'}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export function OnboardingVerifyingStep() {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Verificando tu email, espera un momento...
|
||||
</p>
|
||||
)
|
||||
<p className="text-sm text-muted-foreground">Verificando tu email, espera un momento...</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
onboardingCompleteSchema,
|
||||
onboardingStartSchema,
|
||||
onboardingVerifyOtpSchema,
|
||||
type PlanSummary,
|
||||
} from '@repo/api-contract'
|
||||
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 { 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'
|
||||
} 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 } from 'react-hook-form';
|
||||
|
||||
const OTP_MAX_ATTEMPTS = 5
|
||||
type OnboardStep = 'start' | 'verify-otp' | 'complete'
|
||||
const OTP_MAX_ATTEMPTS = 5;
|
||||
type OnboardStep = 'start' | 'verify-otp' | 'complete';
|
||||
|
||||
export function OnboardPage() {
|
||||
const navigate = useNavigate()
|
||||
const { signInWithPassword } = useAuth()
|
||||
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 [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 startForm = useForm<StartValues>({
|
||||
resolver: zodResolver(onboardingStartSchema),
|
||||
@@ -46,12 +46,10 @@ export function OnboardPage() {
|
||||
fullName: '',
|
||||
email: '',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const completeForm = useForm<CompleteValues>({
|
||||
resolver: zodResolver(
|
||||
onboardingCompleteSchema.omit({ onboardingRequestId: true }),
|
||||
),
|
||||
resolver: zodResolver(onboardingCompleteSchema.omit({ onboardingRequestId: true })),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
password: '',
|
||||
@@ -59,7 +57,7 @@ export function OnboardPage() {
|
||||
physicalAddress: '',
|
||||
planCode: '',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const verifyOtpForm = useForm<VerifyOtpValues>({
|
||||
resolver: zodResolver(onboardingVerifyOtpSchema.omit({ requestId: true })),
|
||||
@@ -67,114 +65,114 @@ export function OnboardPage() {
|
||||
defaultValues: {
|
||||
otp: '',
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 'complete') return
|
||||
if (step !== 'complete') return;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await apiClient.plans.list()
|
||||
setPlans(result)
|
||||
const result = await apiClient.plans.list();
|
||||
setPlans(result);
|
||||
} catch {
|
||||
setPlans([])
|
||||
setPlans([]);
|
||||
}
|
||||
})()
|
||||
}, [step])
|
||||
})();
|
||||
}, [step]);
|
||||
|
||||
const planPlaceholder = useMemo(() => {
|
||||
if (plans.length === 0) return 'Codigo del plan (por ejemplo: BASIC)'
|
||||
return 'Selecciona un plan'
|
||||
}, [plans.length])
|
||||
if (plans.length === 0) return 'Codigo del plan (por ejemplo: BASIC)';
|
||||
return 'Selecciona un plan';
|
||||
}, [plans.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resendCooldownSeconds <= 0) return
|
||||
if (resendCooldownSeconds <= 0) return;
|
||||
|
||||
const timeout = window.setTimeout(() => {
|
||||
setResendCooldownSeconds((current) => Math.max(0, current - 1))
|
||||
}, 1000)
|
||||
setResendCooldownSeconds((current) => Math.max(0, current - 1));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeout)
|
||||
}
|
||||
}, [resendCooldownSeconds])
|
||||
window.clearTimeout(timeout);
|
||||
};
|
||||
}, [resendCooldownSeconds]);
|
||||
|
||||
const onSubmitStart = async (values: StartValues) => {
|
||||
setErrorMessage(null)
|
||||
setInfoMessage(null)
|
||||
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)
|
||||
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.')
|
||||
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('No encontramos una solicitud de onboarding activa.');
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(null)
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const result = await apiClient.onboarding.verifyOtp({
|
||||
requestId,
|
||||
otp: values.otp,
|
||||
})
|
||||
});
|
||||
|
||||
setRemainingAttempts(result.remainingAttempts)
|
||||
setResendCooldownSeconds(result.cooldownSeconds)
|
||||
setInfoMessage(result.message)
|
||||
setRemainingAttempts(result.remainingAttempts);
|
||||
setResendCooldownSeconds(result.cooldownSeconds);
|
||||
setInfoMessage(result.message);
|
||||
|
||||
if (result.verified) {
|
||||
setVerifiedEmail(result.email ?? onboardingEmail)
|
||||
setStep('complete')
|
||||
return
|
||||
setVerifiedEmail(result.email ?? onboardingEmail);
|
||||
setStep('complete');
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.')
|
||||
setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onResendOtp = async () => {
|
||||
if (!requestId) {
|
||||
setErrorMessage('No encontramos una solicitud de onboarding activa.')
|
||||
return
|
||||
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResending(true)
|
||||
setErrorMessage(null)
|
||||
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)
|
||||
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.')
|
||||
setErrorMessage('No pudimos reenviar el OTP. Intenta nuevamente.');
|
||||
} finally {
|
||||
setIsResending(false)
|
||||
setIsResending(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmitComplete = async (values: CompleteValues) => {
|
||||
if (!requestId) {
|
||||
setErrorMessage('No encontramos una solicitud de onboarding activa.')
|
||||
return
|
||||
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(null)
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const result = await apiClient.onboarding.complete({
|
||||
@@ -183,19 +181,19 @@ export function OnboardPage() {
|
||||
complexName: values.complexName,
|
||||
physicalAddress: values.physicalAddress,
|
||||
planCode: values.planCode,
|
||||
})
|
||||
setCurrentComplexSlug(result.complexSlug)
|
||||
});
|
||||
setCurrentComplexSlug(result.complexSlug);
|
||||
|
||||
const emailForSignIn = verifiedEmail ?? onboardingEmail
|
||||
const emailForSignIn = verifiedEmail ?? onboardingEmail;
|
||||
if (emailForSignIn) {
|
||||
await signInWithPassword({ email: emailForSignIn, password: values.password })
|
||||
await signInWithPassword({ email: emailForSignIn, password: values.password });
|
||||
}
|
||||
|
||||
await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } })
|
||||
await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } });
|
||||
} catch {
|
||||
setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.')
|
||||
setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<OnboardingLayout
|
||||
@@ -237,5 +235,5 @@ export function OnboardPage() {
|
||||
/>
|
||||
)}
|
||||
</OnboardingLayout>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,15 +2,9 @@ import {
|
||||
onboardingCompleteSchema,
|
||||
onboardingStartSchema,
|
||||
onboardingVerifyOtpSchema,
|
||||
} from '@repo/api-contract'
|
||||
import { z } from 'zod'
|
||||
} from '@repo/api-contract';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type StartValues = z.infer<typeof onboardingStartSchema>
|
||||
export type VerifyOtpValues = Omit<
|
||||
z.infer<typeof onboardingVerifyOtpSchema>,
|
||||
'requestId'
|
||||
>
|
||||
export type CompleteValues = Omit<
|
||||
z.infer<typeof onboardingCompleteSchema>,
|
||||
'onboardingRequestId'
|
||||
>
|
||||
export type StartValues = z.infer<typeof onboardingStartSchema>;
|
||||
export type VerifyOtpValues = Omit<z.infer<typeof onboardingVerifyOtpSchema>, 'requestId'>;
|
||||
export type CompleteValues = Omit<z.infer<typeof onboardingCompleteSchema>, 'onboardingRequestId'>;
|
||||
|
||||
Reference in New Issue
Block a user