diff --git a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx index 7d732b10..06b96d65 100644 --- a/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx +++ b/packages/ui/src/components/sections/remote-instances/RemoteInstancesPage.tsx @@ -438,6 +438,9 @@ export const RemoteInstancesPage: React.FC = () => { const [remoteClientLabel, setRemoteClientLabel] = React.useState(''); const [remoteClientError, setRemoteClientError] = React.useState(null); const [pairingUrl, setPairingUrl] = React.useState(null); + // The pairing session shown in the QR dialog; used to auto-close the dialog + // once the device redeems it (the pairing leaves the pending list). + const [createdPairingId, setCreatedPairingId] = React.useState(null); const [pairingQrDataUrl, setPairingQrDataUrl] = React.useState(null); const [pairingCopied, setPairingCopied] = React.useState(false); // "Add a device" dialog: a configure phase (name + transport + fallback) then a @@ -721,12 +724,15 @@ export const RemoteInstancesPage: React.FC = () => { if (!options?.silent) setRemoteClientsLoading(true); if (!options?.silent) setRemoteClientError(null); try { + // Pending fetch failure returns null (NOT []) so a transient blip neither + // blanks the pending list nor fakes a "pairing redeemed" signal for the + // QR dialog's auto-close below. const [clients, pending] = await Promise.all([ clientAuth.listClients(), - clientAuth.listPendingPairings().catch(() => [] as PendingPairingRecord[]), + clientAuth.listPendingPairings().catch(() => null), ]); setRemoteClients(clients); - setPendingPairings(pending); + if (pending) setPendingPairings(pending); } catch (err) { // A silent poll must not surface a transient error over the live list. if (!options?.silent) setRemoteClientError(err instanceof Error ? err.message : String(err)); @@ -735,6 +741,30 @@ export const RemoteInstancesPage: React.FC = () => { } }, [clientAuth]); + // Auto-close the QR/link dialog once the device connects: the pairing session + // is single-use, so it leaving the pending list means it was redeemed (or + // expired/cancelled — the dialog is stale either way). Armed only after the + // pairing has been SEEN in the pending list — the result phase renders before + // the refreshed list arrives, and closing on that stale "absent" would blink + // the dialog shut immediately. Successful-fetch-only updates keep transient + // poll failures from faking the disappearance. + const pairingSeenPendingRef = React.useRef(false); + React.useEffect(() => { + if (!addDeviceOpen || addDevicePhase !== 'result' || !createdPairingId) return; + if (pendingPairings.some((pending) => pending.id === createdPairingId)) { + pairingSeenPendingRef.current = true; + return; + } + if (!pairingSeenPendingRef.current) return; + setCreatedPairingId(null); + setAddDeviceOpen(false); + // Celebrate only an actual redeem (a client minted from this pairing exists); + // an expired or cancelled session closes the stale dialog silently. + if (remoteClients.some((client) => client.pairingId === createdPairingId)) { + toast.success(t('settings.remoteInstances.clientAuth.addDevice.connectedToast')); + } + }, [addDeviceOpen, addDevicePhase, createdPairingId, pendingPairings, remoteClients, t]); + const cancelPendingPairing = React.useCallback(async (id: string) => { if (!clientAuth) return; try { @@ -788,6 +818,7 @@ export const RemoteInstancesPage: React.FC = () => { setPairingUrl(null); setPairingQrDataUrl(null); setPairingCopied(false); + setCreatedPairingId(null); setAddDevicePhase('configure'); setAddDeviceFallback(true); setAddDeviceOpen(true); @@ -848,7 +879,11 @@ export const RemoteInstancesPage: React.FC = () => { // E2EE key), so render at high resolution with low error-correction. setPairingQrDataUrl(await QRCode.toDataURL(encoded, { width: 1024, margin: 2, errorCorrectionLevel: 'L' })); setPairingCopied(false); + pairingSeenPendingRef.current = false; + setCreatedPairingId(pairing.id); setAddDevicePhase('result'); + // Loads the pending list including this pairing BEFORE the result phase + // polls it, so the auto-close effect sees "present -> gone" transitions. await loadRemoteClients({ silent: true }); } catch (err) { setRemoteClientError(err instanceof Error ? err.message : String(err)); diff --git a/packages/ui/src/lib/api/types.ts b/packages/ui/src/lib/api/types.ts index 8b19606a..810d849d 100644 --- a/packages/ui/src/lib/api/types.ts +++ b/packages/ui/src/lib/api/types.ts @@ -1109,6 +1109,8 @@ export interface RemoteClientRecord { expiresAt?: string | null; clientKind?: string | null; authMethod?: string | null; + /** Pairing session this client was created from, when authMethod is 'pairing'. */ + pairingId?: string | null; deviceName?: string | null; devicePlatform?: string | null; usesRelay?: boolean; diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 0b744d5a..9bd2acac 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -296,6 +296,7 @@ export const settingsDict = { 'settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal': 'Prefer the direct home connection when available', 'settings.remoteInstances.clientAuth.addDevice.create': 'Create QR code', 'settings.remoteInstances.clientAuth.addDevice.done': 'Done', + 'settings.remoteInstances.clientAuth.addDevice.connectedToast': 'Device connected.', 'settings.remoteInstances.clientAuth.pairingUrl': 'Connection link', 'settings.remoteInstances.clientAuth.createdToken': 'Copy this token now. For security, it will not be shown again.', 'settings.remoteInstances.clientAuth.state.loading': 'Loading tokens...', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index d1cd5942..a3c2c722 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -263,6 +263,7 @@ export const settingsDict = { "settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal": "Preferir la conexión doméstica directa cuando esté disponible", "settings.remoteInstances.clientAuth.addDevice.create": "Crear código QR", "settings.remoteInstances.clientAuth.addDevice.done": "Listo", + "settings.remoteInstances.clientAuth.addDevice.connectedToast": "Dispositivo conectado.", "settings.remoteInstances.clientAuth.pairingUrl": "Enlace de conexión", "settings.remoteInstances.clientAuth.createdToken": "Copia este token ahora. Por seguridad, no se volverá a mostrar.", "settings.remoteInstances.clientAuth.state.loading": "Cargando tokens...", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index 5abbca42..8e8956ce 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1804,6 +1804,7 @@ export const settingsDict = { 'settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal': 'Préférer la connexion domestique directe quand elle est disponible', 'settings.remoteInstances.clientAuth.addDevice.create': 'Créer le code QR', 'settings.remoteInstances.clientAuth.addDevice.done': 'Terminé', + 'settings.remoteInstances.clientAuth.addDevice.connectedToast': 'Appareil connecté.', 'settings.remoteInstances.clientAuth.pairingUrl': 'Lien de connexion', 'settings.remoteInstances.clientAuth.createdToken': 'Copiez ce token maintenant. Pour des raisons de sécurité, il ne sera plus affiché.', 'settings.remoteInstances.clientAuth.state.loading': 'Chargement des tokens...', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index f9d995f8..7ad16683 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -296,6 +296,7 @@ export const settingsDict = { 'settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal': '可能なときは自宅の直接接続を優先', 'settings.remoteInstances.clientAuth.addDevice.create': 'QRコードを作成', 'settings.remoteInstances.clientAuth.addDevice.done': '完了', + 'settings.remoteInstances.clientAuth.addDevice.connectedToast': 'デバイスを接続しました。', 'settings.remoteInstances.clientAuth.pairingUrl': '接続リンク', 'settings.remoteInstances.clientAuth.createdToken': 'この Token を今すぐコピーしてください。セキュリティのため、再表示されません。', 'settings.remoteInstances.clientAuth.state.loading': 'Token を読み込み中...', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 3d112f23..0cb93be3 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -263,6 +263,7 @@ export const settingsDict = { 'settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal': '가능하면 집에서는 직접 연결 우선', 'settings.remoteInstances.clientAuth.addDevice.create': 'QR 코드 만들기', 'settings.remoteInstances.clientAuth.addDevice.done': '완료', + 'settings.remoteInstances.clientAuth.addDevice.connectedToast': '기기가 연결되었습니다.', 'settings.remoteInstances.clientAuth.pairingUrl': '연결 링크', 'settings.remoteInstances.clientAuth.createdToken': '지금 이 토큰을 복사하세요. 보안을 위해 다시 표시되지 않습니다.', 'settings.remoteInstances.clientAuth.state.loading': '토큰을 불러오는 중...', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 2f4bf8d7..8dc34711 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -1492,6 +1492,7 @@ export const settingsDict = { 'settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal': 'Preferuj bezpośrednie połączenie domowe, gdy dostępne', 'settings.remoteInstances.clientAuth.addDevice.create': 'Utwórz kod QR', 'settings.remoteInstances.clientAuth.addDevice.done': 'Gotowe', + 'settings.remoteInstances.clientAuth.addDevice.connectedToast': 'Urządzenie połączone.', 'settings.remoteInstances.clientAuth.pairingUrl': 'Link połączenia', 'settings.remoteInstances.clientAuth.createdToken': 'Skopiuj ten token teraz. Ze względów bezpieczeństwa nie zostanie pokazany ponownie.', 'settings.remoteInstances.clientAuth.state.loading': 'Ładowanie tokenów...', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index 87f2ea65..7cd29130 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -263,6 +263,7 @@ export const settingsDict = { "settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal": "Preferir a conexão doméstica direta quando disponível", "settings.remoteInstances.clientAuth.addDevice.create": "Criar código QR", "settings.remoteInstances.clientAuth.addDevice.done": "Concluído", + "settings.remoteInstances.clientAuth.addDevice.connectedToast": "Dispositivo conectado.", "settings.remoteInstances.clientAuth.pairingUrl": "Link de conexão", "settings.remoteInstances.clientAuth.createdToken": "Copie este token agora. Por segurança, ele não será mostrado novamente.", "settings.remoteInstances.clientAuth.state.loading": "Carregando tokens...", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 473ecc91..e5179e7d 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -263,6 +263,7 @@ export const settingsDict = { "settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal": "Віддавати перевагу прямому домашньому підключенню, коли доступне", "settings.remoteInstances.clientAuth.addDevice.create": "Створити QR-код", "settings.remoteInstances.clientAuth.addDevice.done": "Готово", + "settings.remoteInstances.clientAuth.addDevice.connectedToast": "Пристрій підключено.", "settings.remoteInstances.clientAuth.pairingUrl": "Посилання для підключення", "settings.remoteInstances.clientAuth.createdToken": "Скопіюйте цей токен зараз. З міркувань безпеки він більше не показуватиметься.", "settings.remoteInstances.clientAuth.state.loading": "Завантаження токенів...", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index ac0e9b84..f07da181 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -263,6 +263,7 @@ export const settingsDict = { 'settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal': '在家时优先使用直接连接', 'settings.remoteInstances.clientAuth.addDevice.create': '创建二维码', 'settings.remoteInstances.clientAuth.addDevice.done': '完成', + 'settings.remoteInstances.clientAuth.addDevice.connectedToast': '设备已连接。', 'settings.remoteInstances.clientAuth.pairingUrl': '连接链接', 'settings.remoteInstances.clientAuth.createdToken': '请立即复制此令牌。出于安全考虑,它不会再次显示。', 'settings.remoteInstances.clientAuth.state.loading': '正在加载令牌...', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index dd3875b6..d2eac984 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -269,6 +269,7 @@ 'settings.remoteInstances.clientAuth.addDevice.fallback.preferLocal': '在家時優先使用直接連線', 'settings.remoteInstances.clientAuth.addDevice.create': '建立 QR 代碼', 'settings.remoteInstances.clientAuth.addDevice.done': '完成', + 'settings.remoteInstances.clientAuth.addDevice.connectedToast': '裝置已連線。', 'settings.remoteInstances.clientAuth.pairingUrl': '配對 URL', 'settings.remoteInstances.clientAuth.createdToken': '已建立 token', 'settings.remoteInstances.clientAuth.state.loading': '正在載入用戶端 token...',