From 3d3ae9912572655f55870e7b40ed965491573663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Ben=C3=ADtez?= Date: Sat, 20 Jun 2026 02:08:25 -0300 Subject: [PATCH] =?UTF-8?q?fix(sprint-5/etapa-4b):=20save=20perfil=20via?= =?UTF-8?q?=20fetch=20nativo=20=E2=80=94=20bypasea=20processLock=20compart?= =?UTF-8?q?ido?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/components/ProfileSheet.tsx | 60 +++++++++++++++++++++++++++++---- src/lib/supabase.ts | 9 +++-- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/components/ProfileSheet.tsx b/src/components/ProfileSheet.tsx index f664042..64a1ebe 100644 --- a/src/components/ProfileSheet.tsx +++ b/src/components/ProfileSheet.tsx @@ -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 | 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(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) { + + {error && ( +

{error}

+ )} diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 4f521ee..901e839 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -1,12 +1,15 @@ import { createClient, processLock } from '@supabase/supabase-js' -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY +export const SUPABASE_URL = (import.meta.env.VITE_SUPABASE_URL as string | undefined) ?? '' +export const SUPABASE_ANON_KEY = (import.meta.env.VITE_SUPABASE_ANON_KEY as string | undefined) ?? '' -if (!supabaseUrl || !supabaseAnonKey) { +if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { throw new Error('Variables de entorno VITE_SUPABASE_URL y VITE_SUPABASE_ANON_KEY son requeridas') } +const supabaseUrl = SUPABASE_URL +const supabaseAnonKey = SUPABASE_ANON_KEY + export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { // Por defecto supabase-js v2 usa navigator.locks para coordinar refresh de sesión entre tabs.