fix(sprint-5/etapa-4b): save perfil via fetch nativo — bypasea processLock compartido
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
upsertUserOnLogin + syncProfileFromDB (AuthContext useEffect) adquieren el
processLock de supabase-js al cargar la página. Si el servidor tarda, ese lock
queda ocupado y supabase.from('users').update() en handleSave espera para siempre
en la misma cola — congelando la UI indefinidamente.
Fix: reemplazar el cliente Supabase por fetch nativo en ProfileSheet. El token
se lee directamente de localStorage (donde GoTrueClient lo escribe tras cada
login/refresh), eliminando toda dependencia del processLock. Si el token no
está disponible o el servidor responde con error, se muestra el mensaje inline
en lugar de colgar el botón.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/com
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { SUPABASE_URL, SUPABASE_ANON_KEY } from '@/lib/supabase'
|
||||
import { useAuth } from '@/auth/AuthContext'
|
||||
|
||||
interface ProfileSheetProps {
|
||||
@@ -12,16 +12,34 @@ interface ProfileSheetProps {
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
// Lee el access token de localStorage sin pasar por el processLock de Supabase.
|
||||
// El token es escrito por GoTrueClient tras cada login/refresh y siempre está vigente
|
||||
// mientras la sesión esté activa.
|
||||
function getAccessToken(): string | null {
|
||||
try {
|
||||
const key = Object.keys(localStorage).find(
|
||||
(k) => k.startsWith('sb-') && k.endsWith('-auth-token')
|
||||
)
|
||||
if (!key) return null
|
||||
const stored = JSON.parse(localStorage.getItem(key) ?? 'null') as Record<string, unknown> | null
|
||||
return (stored?.access_token as string | undefined) ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
||||
const { user, updateUser } = useAuth()
|
||||
const [name, setName] = useState('')
|
||||
const [company, setCompany] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && user) {
|
||||
setName(user.name)
|
||||
setCompany(user.company ?? '')
|
||||
setError(null)
|
||||
}
|
||||
// Intencional: solo inicializar cuando el sheet se abre, no en cada cambio de `user`
|
||||
// (evita resetear el formulario mientras el usuario está editando).
|
||||
@@ -31,16 +49,42 @@ export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
||||
async function handleSave() {
|
||||
if (!user || !name.trim()) return
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('users')
|
||||
.update({ name: name.trim(), company: company.trim() || null })
|
||||
.eq('id', user.id)
|
||||
if (error) throw error
|
||||
// Usamos fetch nativo en lugar de supabase.from().update() para evitar que esta
|
||||
// operación quede en la cola del processLock compartido. En el login, upsertUserOnLogin
|
||||
// y syncProfileFromDB adquieren ese lock; si el servidor tarda, la cola queda ocupada
|
||||
// y el save cuelga indefinidamente. El fetch nativo es independiente del lock.
|
||||
const token = getAccessToken()
|
||||
if (!token) throw new Error('Sesión no disponible — recargá la página e intentá de nuevo')
|
||||
|
||||
const res = await fetch(
|
||||
`${SUPABASE_URL}/rest/v1/users?id=eq.${encodeURIComponent(user.id)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
apikey: SUPABASE_ANON_KEY,
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Prefer: 'return=minimal',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
company: company.trim() || null,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => '')
|
||||
throw new Error(`Error ${res.status}${detail ? `: ${detail}` : ''}`)
|
||||
}
|
||||
|
||||
updateUser({ name: name.trim(), company: company.trim() || undefined })
|
||||
onOpenChange(false)
|
||||
} catch (err) {
|
||||
console.error('Error al guardar perfil:', err)
|
||||
setError(err instanceof Error ? err.message : 'Error al guardar perfil')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -77,6 +121,10 @@ export function ProfileSheet({ open, onOpenChange }: ProfileSheetProps) {
|
||||
<Label className="text-xs">Email</Label>
|
||||
<Input value={user?.email ?? ''} readOnly className="bg-muted text-muted-foreground cursor-not-allowed" />
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-destructive">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SheetFooter className="mt-6 flex gap-2">
|
||||
|
||||
Reference in New Issue
Block a user