Some checks failed
/ Deploy to Cloudflare Pages (push) Has been cancelled
El operador ?? solo hace fallback con null/undefined, no con cadena vacía.
Si Google devuelve user_metadata.full_name: "", la cadena de propagación es:
buildAuthUser → name="" → upsertUserOnLogin inserta "" → syncProfileFromDB
devuelve "" → AuthContext setUser → "" ?? prev.name = "" → ProfileSheet
setName("") → name.trim() === '' → botón disabled → click no hace nada.
Fix: cambiar ?? por || en todos los puntos donde full_name o name de DB
se propagan al estado: buildAuthUser, syncProfileFromDB, getUserFromStorage,
useEffect IIFE en AuthContext. Agregar fallback a email en ProfileSheet
setName() para usuarios cuya fila en DB ya tiene name="" de sesiones anteriores.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Check } from 'lucide-react'
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Button } from '@/components/ui/button'
|
|
import { SUPABASE_URL, SUPABASE_ANON_KEY } from '@/lib/supabase'
|
|
import { useAuth } from '@/auth/AuthContext'
|
|
|
|
interface ProfileSheetProps {
|
|
open: boolean
|
|
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 || user.email || '')
|
|
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).
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [open])
|
|
|
|
async function handleSave() {
|
|
if (!user || !name.trim()) return
|
|
setSaving(true)
|
|
setError(null)
|
|
try {
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent>
|
|
<SheetHeader>
|
|
<SheetTitle>Editar perfil</SheetTitle>
|
|
</SheetHeader>
|
|
|
|
<div className="space-y-4 mt-6">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Nombre</Label>
|
|
<Input
|
|
autoFocus
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Tu nombre"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs">Empresa</Label>
|
|
<Input
|
|
value={company}
|
|
onChange={(e) => setCompany(e.target.value)}
|
|
placeholder="InQuality"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<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">
|
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
|
|
Cancelar
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={!name.trim() || saving} className="gap-1.5">
|
|
<Check className="h-4 w-4" />
|
|
Guardar
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|