@@ -23,6 +23,7 @@ import { api } from '@/infrastructure/api/service-api/ApiClient';
2323import { usePeerDeviceMode } from '@/infrastructure/peer-device/PeerDeviceContext' ;
2424import { useAccountSyncStore , ensureAccountSyncProgressListener } from '@/infrastructure/account/accountSyncStore' ;
2525import type { AccountSyncPhase } from '@/infrastructure/account/accountSyncStore' ;
26+ import { isAccountAuthFailure } from '@/infrastructure/account/accountErrorUtils' ;
2627import { useNotification } from '@/shared/notification-system' ;
2728import { createLogger } from '@/shared/utils/logger' ;
2829import './AccountLoginDialog.scss' ;
@@ -63,16 +64,6 @@ function syncPhaseLabel(
6364 }
6465}
6566
66- function isAccountAuthFailure ( error : unknown ) : boolean {
67- const msg = ( error instanceof Error ? error . message : String ( error ) ) . toLowerCase ( ) ;
68- return (
69- msg . includes ( '401' )
70- || msg . includes ( 'unauthorized' )
71- || msg . includes ( 'invalid or expired token' )
72- || msg . includes ( 'relay auth error' )
73- ) ;
74- }
75-
7667interface AccountLoginDialogProps {
7768 isOpen : boolean ;
7869 onClose : ( ) => void ;
@@ -104,13 +95,18 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
10495
10596 const [ devices , setDevices ] = useState < AccountDeviceInfo [ ] > ( [ ] ) ;
10697 const [ localDeviceId , setLocalDeviceId ] = useState < string | null > ( null ) ;
98+ /** Only true after a successful list_devices response; gates the empty-state copy. */
99+ const [ devicesReady , setDevicesReady ] = useState ( false ) ;
100+ const [ relayError , setRelayError ] = useState < string | null > ( null ) ;
107101 const refreshTimer = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
108102 /** Prevent overlapping background syncs from rapid clicks. */
109103 const syncInFlightRef = useRef ( false ) ;
110104
111105 const resetState = useCallback ( ( ) => {
112106 setDevices ( [ ] ) ;
113107 setLocalDeviceId ( null ) ;
108+ setDevicesReady ( false ) ;
109+ setRelayError ( null ) ;
114110 if ( refreshTimer . current ) { clearInterval ( refreshTimer . current ) ; refreshTimer . current = null ; }
115111 } , [ ] ) ;
116112
@@ -125,6 +121,11 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
125121 setError ( t ( 'accountLogin.sessionExpired' ) ) ;
126122 } , [ resetState , t ] ) ;
127123
124+ const markRelayUnreachable = useCallback ( ( ) => {
125+ setDevicesReady ( false ) ;
126+ setRelayError ( t ( 'accountLogin.relayUnreachable' ) ) ;
127+ } , [ t ] ) ;
128+
128129 const refreshDevices = useCallback ( async ( ) => {
129130 try {
130131 let list = await remoteConnectAPI . accountListDevices ( ) ;
@@ -134,13 +135,35 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
134135 list = await remoteConnectAPI . accountListDevices ( ) ;
135136 }
136137 setDevices ( list ) ;
138+ setDevicesReady ( true ) ;
139+ setRelayError ( null ) ;
137140 } catch ( e ) {
138141 log . warn ( 'refreshDevices failed' , e ) ;
139142 if ( isAccountAuthFailure ( e ) ) {
140143 await handleSessionExpired ( e ) ;
144+ } else {
145+ markRelayUnreachable ( ) ;
146+ }
147+ }
148+ } , [ localDeviceId , handleSessionExpired , markRelayUnreachable ] ) ;
149+
150+ const handleRetryConnect = useCallback ( async ( ) => {
151+ setLoading ( true ) ;
152+ setRelayError ( null ) ;
153+ try {
154+ await remoteConnectAPI . accountConnectDevices ( ) ;
155+ await refreshDevices ( ) ;
156+ } catch ( err ) {
157+ log . warn ( 'retry connect failed' , err ) ;
158+ if ( isAccountAuthFailure ( err ) ) {
159+ await handleSessionExpired ( err ) ;
160+ return ;
141161 }
162+ markRelayUnreachable ( ) ;
163+ } finally {
164+ setLoading ( false ) ;
142165 }
143- } , [ localDeviceId , handleSessionExpired ] ) ;
166+ } , [ handleSessionExpired , markRelayUnreachable , refreshDevices ] ) ;
144167
145168 const applyPresenceOnline = useCallback ( ( onlineDevices : Array < { device_id : string ; device_name : string } > ) => {
146169 const onlineIds = new Set ( onlineDevices . map ( d => d . device_id ) ) ;
@@ -195,6 +218,7 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
195218 } ) ;
196219 remoteConnectAPI . accountStatus ( ) . then ( async ( status ) => {
197220 if ( status . logged_in && status . user_id ) {
221+ setView ( 'devices' ) ;
198222 try {
199223 await remoteConnectAPI . accountConnectDevices ( ) ;
200224 } catch ( err ) {
@@ -203,9 +227,9 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
203227 await handleSessionExpired ( err ) ;
204228 return ;
205229 }
230+ markRelayUnreachable ( ) ;
206231 }
207- setView ( 'devices' ) ;
208- refreshDevices ( ) ;
232+ void refreshDevices ( ) ;
209233 startDevicePolling ( ) ;
210234 }
211235 } ) ;
@@ -232,7 +256,15 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
232256 unlistenPresence ( ) ;
233257 unlistenSettings ( ) ;
234258 } ;
235- } , [ isOpen , refreshDevices , resetState , startDevicePolling , applyPresenceOnline , handleSessionExpired ] ) ;
259+ } , [
260+ isOpen ,
261+ refreshDevices ,
262+ resetState ,
263+ startDevicePolling ,
264+ applyPresenceOnline ,
265+ handleSessionExpired ,
266+ markRelayUnreachable ,
267+ ] ) ;
236268
237269 const validate = useCallback ( ( ) => {
238270 if ( ! username . trim ( ) || ! password . trim ( ) || ! authServer . trim ( ) ) {
@@ -516,7 +548,7 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
516548
517549 { view === 'devices' && (
518550 < div className = "account-login-dialog__scroll" >
519- { syncStatus !== 'idle' && (
551+ { syncStatus !== 'idle' && ! relayError && (
520552 < div className = { `account-login-dialog__sync-indicator ${ syncStatus } ` } >
521553 < div className = "account-login-dialog__sync-indicator-row" >
522554 { syncStatus === 'syncing' && < RefreshCw size = { 14 } className = "spinning" /> }
@@ -554,11 +586,20 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
554586 ) }
555587 </ div >
556588 ) }
589+ { relayError && (
590+ < div className = "account-login-dialog__error-banner" >
591+ < Alert
592+ type = "error"
593+ message = { relayError }
594+ className = "account-login-dialog__error-alert"
595+ />
596+ </ div >
597+ ) }
557598 < div className = "account-login-dialog__device-list" >
558- { devices . length === 0 && (
599+ { ! relayError && devicesReady && devices . length === 0 && (
559600 < div className = "account-login-dialog__empty" > { t ( 'accountLogin.noDevices' ) } </ div >
560601 ) }
561- { devices . map ( ( d ) => {
602+ { ! relayError && devices . map ( ( d ) => {
562603 const isLocal = localDeviceId === d . device_id ;
563604 return (
564605 < div key = { d . device_id }
@@ -599,6 +640,12 @@ export const AccountLoginDialog: React.FC<AccountLoginDialogProps> = ({
599640 } ) }
600641 </ div >
601642 < div className = "account-login-dialog__actions" >
643+ { relayError && (
644+ < Button variant = "primary" size = "small" onClick = { handleRetryConnect } disabled = { loading } >
645+ < RefreshCw size = { 14 } />
646+ { t ( 'accountLogin.retryConnect' ) }
647+ </ Button >
648+ ) }
602649 < Button variant = "secondary" size = "small" onClick = { handleLogout } disabled = { loading } >
603650 { t ( 'accountLogin.logout' ) }
604651 </ Button >
0 commit comments