66 */
77
88import { useNavigate } from '@tanstack/react-router' ;
9+ import { useState } from 'react' ;
910import { LogOut , User as UserIcon , Building2 } from 'lucide-react' ;
1011import { useObjectTranslation } from '@object-ui/i18n' ;
1112import {
@@ -19,6 +20,7 @@ import {
1920import { Avatar , AvatarFallback , AvatarImage } from '@/components/ui/avatar' ;
2021import { Button } from '@/components/ui/button' ;
2122import { useSession } from '@/hooks/useSession' ;
23+ import { TransitionOverlay } from '@/components/auth/transition-overlay' ;
2224
2325function initials ( name ?: string | null , email ?: string | null ) : string {
2426 const src = ( name || email || '?' ) . trim ( ) ;
@@ -31,6 +33,7 @@ export function UserMenu() {
3133 const { t } = useObjectTranslation ( ) ;
3234 const navigate = useNavigate ( ) ;
3335 const { user, loading, logout } = useSession ( ) ;
36+ const [ signingOut , setSigningOut ] = useState ( false ) ;
3437
3538 if ( loading && ! user ) {
3639 return < div className = "h-8 w-8 animate-pulse rounded-full bg-muted" aria-hidden /> ;
@@ -50,49 +53,65 @@ export function UserMenu() {
5053 }
5154
5255 const handleLogout = async ( ) => {
56+ // Show overlay *immediately* so the click registers visually even if
57+ // `logout()` and the subsequent navigation take a second or two on a
58+ // slow link. Without this the dropdown closes, the page stays put,
59+ // and the user thinks "nothing happened".
60+ setSigningOut ( true ) ;
5361 try {
5462 await logout ( ) ;
5563 } finally {
56- navigate ( { to : '/login' , replace : true } ) ;
64+ // Hard-navigate. We deliberately don't use the SPA router: the
65+ // session-cleared state still has stale Account-side data, and a
66+ // full reload guarantees every consumer (incl. the embedded
67+ // Console iframe / sibling SPAs) starts from a clean slate.
68+ window . location . assign ( '/_account/login' ) ;
5769 }
5870 } ;
5971
6072 return (
61- < DropdownMenu >
62- < DropdownMenuTrigger asChild >
63- < Button variant = "ghost" size = "icon" className = "h-8 w-8 rounded-full" >
64- < Avatar className = "h-7 w-7" >
65- { user . image ? < AvatarImage src = { user . image } alt = { user . name ?? user . email ?? t ( 'userMenu.user' ) } /> : null }
66- < AvatarFallback className = "text-[11px]" >
67- { initials ( user . name , user . email ) }
68- </ AvatarFallback >
69- </ Avatar >
70- </ Button >
71- </ DropdownMenuTrigger >
72- < DropdownMenuContent align = "end" className = "w-64" >
73- < DropdownMenuLabel >
74- < div className = "flex flex-col gap-0.5" >
75- < span className = "truncate text-sm font-medium" > { user . name || user . email } </ span >
76- { user . email && (
77- < span className = "truncate text-[11px] text-muted-foreground" > { user . email } </ span >
78- ) }
79- </ div >
80- </ DropdownMenuLabel >
81- < DropdownMenuSeparator />
82- < DropdownMenuItem onSelect = { ( ) => navigate ( { to : '/account' } ) } >
83- < UserIcon className = "mr-2 h-3.5 w-3.5" />
84- { t ( 'userMenu.account' ) }
85- </ DropdownMenuItem >
86- < DropdownMenuItem onSelect = { ( ) => navigate ( { to : '/organizations' } ) } >
87- < Building2 className = "mr-2 h-3.5 w-3.5" />
88- { t ( 'userMenu.organizations' ) }
89- </ DropdownMenuItem >
90- < DropdownMenuSeparator />
91- < DropdownMenuItem onSelect = { handleLogout } >
92- < LogOut className = "mr-2 h-3.5 w-3.5" />
93- { t ( 'userMenu.signOut' ) }
94- </ DropdownMenuItem >
95- </ DropdownMenuContent >
96- </ DropdownMenu >
73+ < >
74+ < DropdownMenu >
75+ < DropdownMenuTrigger asChild >
76+ < Button variant = "ghost" size = "icon" className = "h-8 w-8 rounded-full" >
77+ < Avatar className = "h-7 w-7" >
78+ { user . image ? < AvatarImage src = { user . image } alt = { user . name ?? user . email ?? t ( 'userMenu.user' ) } /> : null }
79+ < AvatarFallback className = "text-[11px]" >
80+ { initials ( user . name , user . email ) }
81+ </ AvatarFallback >
82+ </ Avatar >
83+ </ Button >
84+ </ DropdownMenuTrigger >
85+ < DropdownMenuContent align = "end" className = "w-64" >
86+ < DropdownMenuLabel >
87+ < div className = "flex flex-col gap-0.5" >
88+ < span className = "truncate text-sm font-medium" > { user . name || user . email } </ span >
89+ { user . email && (
90+ < span className = "truncate text-[11px] text-muted-foreground" > { user . email } </ span >
91+ ) }
92+ </ div >
93+ </ DropdownMenuLabel >
94+ < DropdownMenuSeparator />
95+ < DropdownMenuItem onSelect = { ( ) => navigate ( { to : '/account' } ) } >
96+ < UserIcon className = "mr-2 h-3.5 w-3.5" />
97+ { t ( 'userMenu.account' ) }
98+ </ DropdownMenuItem >
99+ < DropdownMenuItem onSelect = { ( ) => navigate ( { to : '/organizations' } ) } >
100+ < Building2 className = "mr-2 h-3.5 w-3.5" />
101+ { t ( 'userMenu.organizations' ) }
102+ </ DropdownMenuItem >
103+ < DropdownMenuSeparator />
104+ < DropdownMenuItem onSelect = { handleLogout } disabled = { signingOut } >
105+ < LogOut className = "mr-2 h-3.5 w-3.5" />
106+ { t ( 'userMenu.signOut' ) }
107+ </ DropdownMenuItem >
108+ </ DropdownMenuContent >
109+ </ DropdownMenu >
110+ { signingOut ? (
111+ < TransitionOverlay
112+ message = { t ( 'auth.userMenu.signingOut' , { defaultValue : 'Signing you out…' } ) }
113+ />
114+ ) : null }
115+ </ >
97116 ) ;
98117}
0 commit comments