feat(pairing): auto-close the QR/link dialog once the device connects
The pairing session is single-use, so it leaving the pending list (polled every 5s) means it was redeemed — close the dialog and toast success. Armed only after the pairing has been seen in the pending list, so the stale list at result-phase open can't blink the dialog shut; expired/cancelled sessions close it silently. Pending-list polling now preserves the previous list on a transient fetch failure instead of blanking it (which would also have faked the redeem signal).
This commit is contained in:
@@ -438,6 +438,9 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
const [remoteClientLabel, setRemoteClientLabel] = React.useState('');
|
||||
const [remoteClientError, setRemoteClientError] = React.useState<string | null>(null);
|
||||
const [pairingUrl, setPairingUrl] = React.useState<string | null>(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<string | null>(null);
|
||||
const [pairingQrDataUrl, setPairingQrDataUrl] = React.useState<string | null>(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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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 を読み込み中...',
|
||||
|
||||
@@ -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': '토큰을 불러오는 중...',
|
||||
|
||||
@@ -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...',
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "Завантаження токенів...",
|
||||
|
||||
@@ -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': '正在加载令牌...',
|
||||
|
||||
@@ -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...',
|
||||
|
||||
Reference in New Issue
Block a user