'use client' import { useEffect, useState } from 'react' import { createBrowserClient } from '@supabase/ssr' import { useRouter } from 'next/navigation' type Stage = 'validating' | 'ready' | 'submitting' | 'error' const DEFAULT_LINK_ERROR = 'Pedile al administrador que te envíe una nueva invitación.' // GoTrue redirige con #error=...&error_code=...&error_description=... cuando // el token ya fue usado, venció, o es inválido — en vez de emitir PASSWORD_RECOVERY. // Mapeo a mensajes accionables; default cubre códigos no contemplados acá. function describeLinkError(errorCode: string | null): string { switch (errorCode) { case 'otp_expired': return 'Este link ya fue usado o venció. ' + DEFAULT_LINK_ERROR case 'access_denied': return 'El acceso fue denegado para este link. ' + DEFAULT_LINK_ERROR default: return 'El link de invitación no es válido. ' + DEFAULT_LINK_ERROR } } export default function ConfirmPage() { const [stage, setStage] = useState('validating') const [error, setError] = useState(null) const [linkErrorMessage, setLinkErrorMessage] = useState(DEFAULT_LINK_ERROR) const [showPassword, setShowPassword] = useState(false) const router = useRouter() const supabase = createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) useEffect(() => { // GoTrue ya nos dice en el fragmento si el token es inválido/vencido/usado // (#error=...&error_code=...) — leerlo evita esperar el timeout genérico. const hash = window.location.hash.startsWith('#') ? window.location.hash.slice(1) : window.location.hash const hashParams = new URLSearchParams(hash) const errorCode = hashParams.get('error_code') if (errorCode) { setLinkErrorMessage(describeLinkError(errorCode)) setStage('error') return } // Supabase emite PASSWORD_RECOVERY tanto para recovery como para invite // cuando el token de la URL (#access_token) es válido. const { data: listener } = supabase.auth.onAuthStateChange((event) => { if (event === 'PASSWORD_RECOVERY') setStage('ready') }) // Carrera posible: si el token ya se procesó antes de montar el listener, // ya hay sesión activa aunque el evento no se vuelva a emitir. supabase.auth.getSession().then(({ data: { session } }) => { setStage((s) => (s === 'validating' && session ? 'ready' : s)) }) // Sin evento ni sesión tras unos segundos = token inválido/vencido/ausente // sin que GoTrue haya mandado un error explícito en el fragmento. const timeout = setTimeout(() => { setStage((s) => (s === 'validating' ? 'error' : s)) }, 8000) return () => { listener.subscription.unsubscribe() clearTimeout(timeout) } }, [supabase]) async function handleSubmit(e: React.FormEvent) { e.preventDefault() const fd = new FormData(e.currentTarget) const password = String(fd.get('password') ?? '') const confirmPassword = String(fd.get('confirmPassword') ?? '') if (password.length < 8) { setError('La contraseña debe tener al menos 8 caracteres.') return } if (password !== confirmPassword) { setError('Las contraseñas no coinciden.') return } setError(null) setStage('submitting') const { error: updateErr } = await supabase.auth.updateUser({ password }) if (updateErr) { setError('No pudimos guardar tu contraseña. Probá de nuevo.') setStage('ready') return } router.push('/dashboard') router.refresh() } if (stage === 'validating') { return (

Validando tu invitación...

) } if (stage === 'error') { return (

Link inválido o vencido

{linkErrorMessage}

) } return (

Creá tu contraseña

{error && (

{error}

)}
) }