diff --git a/src/auth/AuthContext.tsx b/src/auth/AuthContext.tsx index 32824d6..13c5f97 100644 --- a/src/auth/AuthContext.tsx +++ b/src/auth/AuthContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react' +import { createContext, useContext, useEffect, useRef, useState, useCallback, type ReactNode } from 'react' import { SupabaseAuthService } from './SupabaseAuthService' import { supabase } from '@/lib/supabase' import type { AuthUser } from './types' @@ -67,18 +67,30 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [loading, setLoading] = useState(isOAuthRedirect) const [pendingPasswordReset, setPendingPasswordReset] = useState(false) const [isProfileLoaded, setIsProfileLoaded] = useState(false) + // Ref para comparar id entre eventos de auth y NO resetear isProfileLoaded en eventos + // como TOKEN_REFRESHED que llegan con el mismo usuario. Sin esto, si el evento llega + // después de que syncProfileFromDB completó, isProfileLoaded queda en false para siempre. + const lastSyncedUserIdRef = useRef( + isOAuthRedirect ? null : (getUserFromStorage()?.id ?? null) + ) useEffect(() => { // Verificar y sincronizar con Supabase en background. // Si la sesión expiró o fue revocada, onAuthStateChange llamará callback(null) // y el router redirigirá a /login. const unsubscribe = authService.onAuthStateChange((u) => { + const prevId = lastSyncedUserIdRef.current + lastSyncedUserIdRef.current = u?.id ?? null setUser(u) setLoading(false) - // Si no hay usuario, no hay nada que sincronizar → marcar como cargado. - // Si hay usuario, la sincronización con DB reseteará a false → true al completar. - if (!u) setIsProfileLoaded(true) - else setIsProfileLoaded(false) + if (!u) { + // Logout → nada que sincronizar + setIsProfileLoaded(true) + } else if (u.id !== prevId) { + // Usuario diferente → requiere nueva sincronización con DB + setIsProfileLoaded(false) + } + // Mismo usuario (TOKEN_REFRESHED, INITIAL_SESSION, etc.) → preservar isProfileLoaded }) return () => unsubscribe() }, []) diff --git a/src/features/workspace/WorkspacePage.tsx b/src/features/workspace/WorkspacePage.tsx index 20aebff..62d3f3c 100644 --- a/src/features/workspace/WorkspacePage.tsx +++ b/src/features/workspace/WorkspacePage.tsx @@ -68,12 +68,17 @@ export function WorkspacePage() { } }, [user, processId, navigate]) - // Nombre de la organización dueña del proceso (para el back button contextual) - const [orgName, setOrgName] = useState(null) + // Nombre de la organización dueña del proceso (para el back button contextual). + // undefined = todavía cargando, null = cargado sin resultado, string = nombre real. + const [orgName, setOrgName] = useState(undefined) useEffect(() => { if (!currentProcess?.orgId) { setOrgName(null); return } - supabase.from('organizations').select('name').eq('id', currentProcess.orgId).single() - .then(({ data }) => { if (data) setOrgName(data.name as string) }) + supabase + .from('organizations') + .select('name') + .eq('id', currentProcess.orgId) + .maybeSingle() + .then(({ data }) => setOrgName(data?.name ?? null)) }, [currentProcess?.orgId]) // Validación XOR: todos los gateways exclusivos deben sumar 1.0 @@ -196,9 +201,7 @@ export function WorkspacePage() { const isClientRole = user?.platformRole === 'client_editor' const backLabel = isClientRole ? 'Procesos' - : currentProcess.orgId - ? (orgName ?? 'Volver') - : 'Biblioteca' + : orgName ?? 'Biblioteca' function handleBack() { const orgId = currentProcess?.orgId diff --git a/tests/features/workspace/workspace-back-button.test.ts b/tests/features/workspace/workspace-back-button.test.ts index bc19589..f530cf0 100644 --- a/tests/features/workspace/workspace-back-button.test.ts +++ b/tests/features/workspace/workspace-back-button.test.ts @@ -9,14 +9,12 @@ type PlatformRole = 'platform_admin' | 'member' | 'client_editor' | 'client_view function resolveBackButton( platformRole: PlatformRole | undefined, orgId: string | null, - orgName: string | null, + orgName: string | null | undefined, // undefined = cargando, null = sin resultado ): { backLabel: string; backTo: string } { const isClientRole = platformRole === 'client_editor' || platformRole === 'client_viewer' const backLabel = isClientRole ? 'Procesos' - : orgId - ? (orgName ?? 'Volver') - : 'Biblioteca' + : orgName ?? 'Biblioteca' const backTo = orgId && !isClientRole ? `/library?org=${orgId}` : '/library' return { backLabel, backTo } } @@ -28,9 +26,9 @@ describe('workspace back button — lógica de backLabel y backTo', () => { expect(backTo).toBe('/library?org=org-uuid-123') }) - it('2. admin con proceso de cliente mientras orgName carga → muestra "Volver" como fallback', () => { - const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', null) - expect(backLabel).toBe('Volver') + it('2. admin con proceso de cliente mientras orgName carga → muestra "Biblioteca" como fallback', () => { + const { backLabel, backTo } = resolveBackButton('platform_admin', 'org-uuid-123', undefined) + expect(backLabel).toBe('Biblioteca') expect(backTo).toBe('/library?org=org-uuid-123') })