diff --git a/apps/desktop/src/components/AppShell.tsx b/apps/desktop/src/components/AppShell.tsx index 0c05ab6..a438a61 100644 --- a/apps/desktop/src/components/AppShell.tsx +++ b/apps/desktop/src/components/AppShell.tsx @@ -7,7 +7,6 @@ import { startDeviceApprovalListener } from '../lib/deviceApproval'; import { ensureInstallId } from '../lib/installId'; import { ensureNotificationPermission } from '../lib/osNotify'; import { cachedUserKey } from '../lib/userIdentity'; -import { BackupPromptBanner } from './BackupPromptBanner'; import { CallUI } from './CallUI'; import { DeviceApprovalBanner } from './DeviceApprovalBanner'; import { Sidebar } from './Sidebar'; @@ -53,7 +52,6 @@ export function AppShell() { - ); diff --git a/apps/desktop/src/components/BackupExportDialog.tsx b/apps/desktop/src/components/BackupExportDialog.tsx deleted file mode 100644 index bd07b2a..0000000 --- a/apps/desktop/src/components/BackupExportDialog.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { type BackupBundle, exportDeviceBackupWithRecovery } from '../lib/deviceBackup'; -import { AlertIcon, CopyIcon, LockIcon, ShieldIcon, SpinnerIcon, XIcon } from './icons'; - -interface Props { - open: boolean; - userId: string; - deviceId: string; - privateKey: Uint8Array; - onClose: () => void; -} - -// Exports the device's private key + identity into a passphrase-protected -// portable string. The user can store this string anywhere (password manager, -// printed paper, encrypted file on a USB stick). Without it, losing local -// storage on this install means losing all past conversation keys. -export function BackupExportDialog({ open, userId, deviceId, privateKey, onClose }: Props) { - const { t } = useTranslation(['app']); - const [passphrase, setPassphrase] = useState(''); - const [confirm, setConfirm] = useState(''); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [bundle, setBundle] = useState(null); - const [copied, setCopied] = useState(false); - const [copiedRecovery, setCopiedRecovery] = useState(false); - - const canGenerate = useMemo(() => { - return passphrase.length >= 8 && passphrase === confirm && !busy; - }, [passphrase, confirm, busy]); - - const reset = useCallback(() => { - setPassphrase(''); - setConfirm(''); - setBundle(null); - setError(null); - setCopied(false); - setCopiedRecovery(false); - }, []); - - const handleClose = useCallback(() => { - reset(); - onClose(); - }, [reset, onClose]); - - const handleGenerate = useCallback(async () => { - if (!canGenerate) return; - setBusy(true); - setError(null); - try { - const b = await exportDeviceBackupWithRecovery({ - userId, - deviceId, - privateKey, - passphrase, - }); - setBundle(b); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - }, [canGenerate, userId, deviceId, privateKey, passphrase]); - - const copyText = async (text: string, marker: 'main' | 'recovery'): Promise => { - try { - await navigator.clipboard.writeText(text); - if (marker === 'main') { - setCopied(true); - window.setTimeout(() => setCopied(false), 1500); - } else { - setCopiedRecovery(true); - window.setTimeout(() => setCopiedRecovery(false), 1500); - } - } catch { - /* fall back — user can select manually */ - } - }; - - const handleDownload = useCallback(() => { - if (!bundle) return; - const text = - '=== Passphrase backup ===\n' + - bundle.passphraseBackup + - '\n\n=== Recovery code ===\n' + - bundle.recoveryCode + - '\n\n=== Recovery backup (use with the recovery code) ===\n' + - bundle.recoveryBackup + - '\n'; - const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `chatapp-device-backup-${deviceId.slice(0, 8)}.txt`; - a.click(); - URL.revokeObjectURL(url); - }, [bundle, deviceId]); - - if (!open) return null; - - return ( -
-
e.stopPropagation()} - className="flex max-h-[90vh] w-full max-w-lg flex-col overflow-hidden rounded-2xl border border-line bg-surface-3 shadow-xl" - > -
-
- -

- {t('app:backup.export_title', { defaultValue: 'Gerätesicherung erstellen' })} -

-
- -
- -
- {!bundle ? ( - <> -

- {t('app:backup.export_explainer', { - defaultValue: - 'Verschlüssele den Geräteschlüssel mit einer Passphrase. Ohne Passphrase UND Backup-String ist keine Wiederherstellung möglich.', - })} -

- -
-
- - setPassphrase(e.target.value)} - placeholder="min. 8 Zeichen" - className="w-full rounded-lg border border-line bg-surface-2 px-3 py-2.5 text-sm text-fg placeholder-fg-muted transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30" - /> -
-
- - setConfirm(e.target.value)} - className="w-full rounded-lg border border-line bg-surface-2 px-3 py-2.5 text-sm text-fg placeholder-fg-muted transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30" - /> -
- {confirm.length > 0 && confirm !== passphrase && ( -

- {t('app:backup.passphrase_mismatch', { defaultValue: 'Passphrasen stimmen nicht überein.' })} -

- )} -
- -
-
- - - {t('app:backup.export_warning', { - defaultValue: - 'Anthropic: Backup + Passphrase sicher aufbewahren. Passphrase kann nicht wiederhergestellt werden.', - })} - -
-
- - {error && ( -

- {error} -

- )} - - ) : ( - <> -
-
- - - {t('app:backup.export_success', { - defaultValue: - 'Backup erstellt. Speichere diesen String + Passphrase in einem Passwortmanager oder drucke ihn aus.', - })} - -
-
- -