import { useForm } from "@tanstack/react-form"; import { Link, createFileRoute } from "@tanstack/react-router"; import { AuthPageCard, AuthPageShell, authRedirectSearchSchema, } from "@/client/features/auth/AuthPage"; import { getFieldError, getFormError } from "@/client/lib/forms"; import { authClient } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { getSignInSearch, normalizeAuthRedirect } from "@/lib/auth-redirect"; import { HOSTED_PASSWORD_MAX_LENGTH, HOSTED_PASSWORD_MIN_LENGTH, } from "@/lib/auth-options"; import { z } from "zod"; const resetPasswordSchema = z .object({ password: z .string() .min( HOSTED_PASSWORD_MIN_LENGTH, `Password must be at least ${HOSTED_PASSWORD_MIN_LENGTH} characters.`, ) .max( HOSTED_PASSWORD_MAX_LENGTH, `Password must be at most ${HOSTED_PASSWORD_MAX_LENGTH} characters.`, ), confirmPassword: z.string(), }) .refine((value) => value.password === value.confirmPassword, { message: "Passwords do not match.", path: ["confirmPassword"], }); const resetPasswordSearchSchema = authRedirectSearchSchema.extend({ error: z.string().optional(), token: z.string().optional(), }); export const Route = createFileRoute("/reset-password")({ validateSearch: resetPasswordSearchSchema, component: ResetPasswordPage, }); function getResetPasswordErrorMessage(error: string | undefined) { switch ((error ?? "").toLowerCase()) { case "invalid_token": return "This reset link is no longer valid. Request a new one to keep going."; case "token_expired": return "This reset link has expired. Request a new one to keep going."; default: return error ? "This reset link can't be used anymore. Request a new one and try again." : null; } } function getResetPasswordPageCopy({ isHostedMode, isComplete, routeError, hasToken, }: { isHostedMode: boolean; isComplete: boolean; routeError: string | null; hasToken: boolean; }) { if (!isHostedMode) { return { title: "Reset password", helperText: "Password reset isn't available right now.", }; } if (isComplete) { return { title: "Password updated", helperText: "Your password has been updated. Sign in with your new password.", }; } if (routeError || !hasToken) { return { title: "Reset link expired", helperText: routeError || "This reset link is no longer valid. Request a new one to keep going.", }; } return { title: "Reset password", helperText: "Choose a new password for your account.", }; } function ResetPasswordPage() { const search = Route.useSearch(); const redirectTo = normalizeAuthRedirect(search.redirect); const isHostedMode = isHostedClientAuthMode(); const routeError = getResetPasswordErrorMessage(search.error); const token = typeof search.token === "string" ? search.token : null; const form = useForm({ defaultValues: { password: "", confirmPassword: "", }, validators: { onSubmit: resetPasswordSchema, }, onSubmit: async ({ formApi, value }) => { if (!token) { formApi.setErrorMap({ onSubmit: { form: "This reset link is no longer valid. Request a new one and try again.", fields: {}, }, }); return; } try { const result = await authClient.resetPassword({ newPassword: value.password, token, }); if (result.error) { formApi.setErrorMap({ onSubmit: { form: "This reset link is no longer valid. Request a new one and try again.", fields: {}, }, }); return; } } catch { formApi.setErrorMap({ onSubmit: { form: "We couldn't update your password right now. Please try again.", fields: {}, }, }); } }, }); return ( ({ isComplete: state.isSubmitSuccessful && !state.errorMap.onSubmit, submitError: state.errorMap.onSubmit, isSubmitting: state.isSubmitting, })} > {({ isComplete, submitError, isSubmitting }) => { const errorMessage = getFormError(submitError); const pageCopy = getResetPasswordPageCopy({ isHostedMode, isComplete, routeError, hasToken: !!token, }); return ( Sign in

} > {!isHostedMode ? null : isComplete ? ( Continue to sign in ) : routeError || !token ? ( Request a new reset link ) : (
{ event.preventDefault(); void form.handleSubmit(); }} > {(field) => { const error = getFieldError(field.state.meta.errors); return (
field.handleChange(event.target.value) } autoComplete="new-password" minLength={HOSTED_PASSWORD_MIN_LENGTH} maxLength={HOSTED_PASSWORD_MAX_LENGTH} required /> {error ? (

{error}

) : null}
); }}
{(field) => { const error = getFieldError(field.state.meta.errors); return (
field.handleChange(event.target.value) } autoComplete="new-password" minLength={HOSTED_PASSWORD_MIN_LENGTH} maxLength={HOSTED_PASSWORD_MAX_LENGTH} required /> {error ? (

{error}

) : null}
); }}
{errorMessage ? (

{errorMessage}

) : null}
)}
); }}
); }