From 10c3d3cae576a671a9963fe40eecdcf78f4e238a Mon Sep 17 00:00:00 2001 From: Sam Babbage Date: Fri, 8 May 2026 11:32:57 -0700 Subject: [PATCH 1/8] fix(storage): repair config Apply, backup add, and add runtime primary swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles closely-related fixes and one feature addition for the storage configuration flow. All surface together when actually using the Settings storage panel against a self-hosted storage server. WalletConfig — Apply now persists across panel close Previously the toggle/close path always re-applied `backupConfig` (the pre-edit snapshot), silently reverting the user's just-applied config. We now clear `backupConfig` on a successful Apply, and in controlled mode also signal the parent via `onToggle()` so the panel auto-closes. WalletService — `addBackupStorageUrl` no longer throws "Originator is required" `_managers.wallet` is the `WalletPermissionsManager`-wrapped wallet, but the BRC-103 handshake inside `StorageClient.makeAvailable()` calls `wallet.createHmac` directly with no originator. The permissions wrapper rejects that. We now stash the raw `wallet` as `underlyingWallet` in `_managers` (alongside the permissioned `wallet`) and use it for the StorageClient constructor. Internal-only; never exposed to apps. Settings — silent failures on Add Backup are now visible The catch block had a comment claiming the service layer toasted errors itself, but the service only toasts on success. Failed adds are now surfaced via `toast.error` and `console.error`. Feature: Make Primary button on each backup row New `WalletService.setPrimaryStorage(target)` calls `WalletStorageManager.setActive` — which syncs pending writes to the target then atomically flips the active store — and updates `useRemoteStorage` / `storageUrl` / `backupStorageUrls` in the snapshot together so the swap survives reload. Wired through `WalletContext` and `useWalletService`. New "Make Primary" button on each backup row in Settings. i18n: `backup_storage_make_primary_button` and `backup_error_add_failed` added across all 12 supported languages. --- src/lib/WalletContext.tsx | 2 + src/lib/components/WalletConfig.tsx | 16 ++++- src/lib/hooks/useWalletService.ts | 5 ++ src/lib/i18n/translations.ts | 24 +++++++ src/lib/pages/Dashboard/Settings/index.tsx | 37 +++++++++- src/lib/services/WalletService.ts | 81 +++++++++++++++++++++- 6 files changed, 157 insertions(+), 8 deletions(-) diff --git a/src/lib/WalletContext.tsx b/src/lib/WalletContext.tsx index 13d8091..89f72f4 100644 --- a/src/lib/WalletContext.tsx +++ b/src/lib/WalletContext.tsx @@ -161,6 +161,7 @@ export interface WalletContextValue { addBackupStorageUrl: (url: string) => Promise; removeBackupStorageUrl: (url: string) => Promise; syncBackupStorage: (progressCallback?: (message: string) => void) => Promise; + setPrimaryStorage: (target: string, progressCallback?: (message: string) => void) => Promise; updateMessageBoxUrl: (url: string) => Promise; removeMessageBoxUrl: () => Promise; initializingBackendServices: boolean; @@ -221,6 +222,7 @@ export const WalletContext = createContext({ addBackupStorageUrl: async () => {}, removeBackupStorageUrl: async () => {}, syncBackupStorage: async () => {}, + setPrimaryStorage: async () => {}, updateMessageBoxUrl: async () => {}, removeMessageBoxUrl: async () => {}, initializingBackendServices: false, diff --git a/src/lib/components/WalletConfig.tsx b/src/lib/components/WalletConfig.tsx index 33cb242..8c951ae 100644 --- a/src/lib/components/WalletConfig.tsx +++ b/src/lib/components/WalletConfig.tsx @@ -122,8 +122,20 @@ const WalletConfig: React.FC = ({ autoExpand = false, hideLog useRemoteStorage, useMessageBox, }) - if (valid) setShowWalletConfig(false) - }, [wabUrl, wabInfo, method, network, storageUrl, messageBoxUrl, loginType, useRemoteStorage, useMessageBox, finalizeConfig, setShowWalletConfig]) + if (valid) { + // The toggle/close path calls `resetCurrentConfig()`, which re-applies `backupConfig` + // (the pre-edit snapshot). After a successful Apply we must clear `backupConfig` so + // that a subsequent close — including the parent's auto-close below — doesn't + // immediately revert the freshly-applied configuration. + setBackupConfig(undefined) + if (isControlled) { + // Parent owns visibility; signal it to close the panel automatically. + onToggle?.() + } else { + setShowWalletConfig(false) + } + } + }, [wabUrl, wabInfo, method, network, storageUrl, messageBoxUrl, loginType, useRemoteStorage, useMessageBox, finalizeConfig, setShowWalletConfig, isControlled, onToggle]) // Force the manager to use the "presentation-key-and-password" flow (only for WAB/CWIStyle managers): useEffect(() => { diff --git a/src/lib/hooks/useWalletService.ts b/src/lib/hooks/useWalletService.ts index 00132b1..98b0f0c 100644 --- a/src/lib/hooks/useWalletService.ts +++ b/src/lib/hooks/useWalletService.ts @@ -251,6 +251,10 @@ export function useWalletService() { const addBackupStorageUrl = useCallback((url: string) => svc.addBackupStorageUrl(url), [svc]) const removeBackupStorageUrl = useCallback((url: string) => svc.removeBackupStorageUrl(url), [svc]) const syncBackupStorage = useCallback((cb?: any) => svc.syncBackupStorage(cb), [svc]) + const setPrimaryStorage = useCallback( + (target: string, cb?: (message: string) => void) => svc.setPrimaryStorage(target, cb), + [svc] + ) const updateMessageBoxUrl = useCallback((url: string) => svc.updateMessageBoxUrl(url), [svc]) const removeMessageBoxUrl = useCallback(() => svc.removeMessageBoxUrl(), [svc]) const updateSettings = useCallback((s: any) => svc.updateSettings(s), [svc]) @@ -375,6 +379,7 @@ export function useWalletService() { addBackupStorageUrl, removeBackupStorageUrl, syncBackupStorage, + setPrimaryStorage, updateMessageBoxUrl, removeMessageBoxUrl, initializingBackendServices: walletState.initializingBackendServices, diff --git a/src/lib/i18n/translations.ts b/src/lib/i18n/translations.ts index 13a1b30..2406e2c 100644 --- a/src/lib/i18n/translations.ts +++ b/src/lib/i18n/translations.ts @@ -95,6 +95,8 @@ const en = { backup_storage_providers_label: 'Backup Storage Providers ({{count}})', backup_storage_local_electron: 'Local Electron Storage (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'Remove', + backup_storage_make_primary_button: 'Make Primary', + backup_error_add_failed: 'Failed to add backup storage: {{message}}', backup_storage_add_button: 'Add Backup Storage', backup_storage_sync_button: 'Sync All Backups', backup_storage_sync_syncing: 'Syncing...', @@ -1077,6 +1079,8 @@ const es = { backup_storage_providers_label: 'Proveedores de Almacenamiento de Respaldo ({{count}})', backup_storage_local_electron: 'Almacenamiento Local Electron (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'Eliminar', + backup_storage_make_primary_button: 'Hacer Primario', + backup_error_add_failed: 'Error al añadir almacenamiento de respaldo: {{message}}', backup_storage_add_button: 'Agregar Almacenamiento de Respaldo', backup_storage_sync_button: 'Sincronizar Todos los Respaldos', backup_storage_sync_syncing: 'Sincronizando...', @@ -2001,6 +2005,8 @@ const fr = { backup_storage_providers_label: 'Fournisseurs de Stockage de Sauvegarde ({{count}})', backup_storage_local_electron: 'Stockage Electron Local (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'Supprimer', + backup_storage_make_primary_button: 'Définir comme Principal', + backup_error_add_failed: 'Échec de l\'ajout du stockage de sauvegarde : {{message}}', backup_storage_add_button: 'Ajouter un Stockage de Sauvegarde', backup_storage_sync_button: 'Synchroniser Toutes les Sauvegardes', backup_storage_sync_syncing: 'Synchronisation...', @@ -2949,6 +2955,8 @@ const pt = { backup_storage_providers_label: 'Provedores de Armazenamento de Backup ({{count}})', backup_storage_local_electron: 'Armazenamento Electron Local (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'Remover', + backup_storage_make_primary_button: 'Tornar Primário', + backup_error_add_failed: 'Falha ao adicionar armazenamento de backup: {{message}}', backup_storage_add_button: 'Adicionar Armazenamento de Backup', backup_storage_sync_button: 'Sincronizar Todos os Backups', backup_storage_sync_syncing: 'Sincronizando...', @@ -3885,6 +3893,8 @@ const zh = { backup_storage_providers_label: '备份存储提供商({{count}})', backup_storage_local_electron: '本地 Electron 存储(~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: '移除', + backup_storage_make_primary_button: '设为主要', + backup_error_add_failed: '添加备份存储失败:{{message}}', backup_storage_add_button: '添加备份存储', backup_storage_sync_button: '同步所有备份', backup_storage_sync_syncing: '同步中...', @@ -4821,6 +4831,8 @@ const hi = { backup_storage_providers_label: 'बैकअप स्टोरेज प्रदाता ({{count}})', backup_storage_local_electron: 'स्थानीय Electron स्टोरेज (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'हटाएं', + backup_storage_make_primary_button: 'प्राथमिक बनाएं', + backup_error_add_failed: 'बैकअप स्टोरेज जोड़ने में विफल: {{message}}', backup_storage_add_button: 'बैकअप स्टोरेज जोड़ें', backup_storage_sync_button: 'सभी बैकअप सिंक करें', backup_storage_sync_syncing: 'सिंक हो रहा है...', @@ -5757,6 +5769,8 @@ const bn = { backup_storage_providers_label: 'ব্যাকআপ স্টোরেজ প্রদানকারী ({{count}})', backup_storage_local_electron: 'স্থানীয় Electron স্টোরেজ (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'সরান', + backup_storage_make_primary_button: 'প্রাথমিক করুন', + backup_error_add_failed: 'ব্যাকআপ স্টোরেজ যোগ করতে ব্যর্থ: {{message}}', backup_storage_add_button: 'ব্যাকআপ স্টোরেজ যোগ করুন', backup_storage_sync_button: 'সব ব্যাকআপ সিঙ্ক করুন', backup_storage_sync_syncing: 'সিঙ্ক হচ্ছে...', @@ -6693,6 +6707,8 @@ const ar = { backup_storage_providers_label: 'مزودو التخزين الاحتياطي ({{count}})', backup_storage_local_electron: 'التخزين المحلي Electron (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'إزالة', + backup_storage_make_primary_button: 'تعيين كأساسي', + backup_error_add_failed: 'فشل إضافة تخزين النسخة الاحتياطية: {{message}}', backup_storage_add_button: 'إضافة تخزين احتياطي', backup_storage_sync_button: 'مزامنة جميع النسخ الاحتياطية', backup_storage_sync_syncing: 'جارٍ المزامنة...', @@ -7629,6 +7645,8 @@ const ru = { backup_storage_providers_label: 'Провайдеры резервного хранилища ({{count}})', backup_storage_local_electron: 'Локальное хранилище Electron (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'Удалить', + backup_storage_make_primary_button: 'Сделать основным', + backup_error_add_failed: 'Не удалось добавить резервное хранилище: {{message}}', backup_storage_add_button: 'Добавить резервное хранилище', backup_storage_sync_button: 'Синхронизировать все резервные копии', backup_storage_sync_syncing: 'Синхронизация...', @@ -8565,6 +8583,8 @@ const id = { backup_storage_providers_label: 'Penyedia Penyimpanan Cadangan ({{count}})', backup_storage_local_electron: 'Penyimpanan Lokal Electron (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'Hapus', + backup_storage_make_primary_button: 'Jadikan Utama', + backup_error_add_failed: 'Gagal menambahkan penyimpanan cadangan: {{message}}', backup_storage_add_button: 'Tambah Penyimpanan Cadangan', backup_storage_sync_button: 'Sinkronkan Semua Cadangan', backup_storage_sync_syncing: 'Menyinkronkan...', @@ -9506,6 +9526,8 @@ const ja = { backup_storage_providers_label: 'バックアップストレージプロバイダー ({{count}})', backup_storage_local_electron: 'ローカルElectronストレージ (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: '削除', + backup_storage_make_primary_button: 'プライマリに設定', + backup_error_add_failed: 'バックアップストレージの追加に失敗しました: {{message}}', backup_storage_add_button: 'バックアップストレージを追加', backup_storage_sync_button: 'すべてのバックアップを同期', backup_storage_sync_syncing: '同期中...', @@ -10488,6 +10510,8 @@ const pl = { backup_storage_providers_label: 'Dostawcy kopii zapasowej ({{count}})', backup_storage_local_electron: 'Lokalna pamięć Electron (~/.bsv-desktop/wallet-*.db)', backup_storage_remove_button: 'Usuń', + backup_storage_make_primary_button: 'Ustaw jako główne', + backup_error_add_failed: 'Nie udało się dodać pamięci kopii zapasowej: {{message}}', backup_storage_add_button: 'Dodaj kopię zapasową', backup_storage_sync_button: 'Synchronizuj wszystkie kopie', backup_storage_sync_syncing: 'Synchronizuję...', diff --git a/src/lib/pages/Dashboard/Settings/index.tsx b/src/lib/pages/Dashboard/Settings/index.tsx index 8160a15..8be287a 100644 --- a/src/lib/pages/Dashboard/Settings/index.tsx +++ b/src/lib/pages/Dashboard/Settings/index.tsx @@ -78,7 +78,7 @@ const Settings = () => { const classes = useStyles() const { t } = useTranslation() const { currentLanguage, setCurrentLanguage, supportedLanguages } = useLanguage() - const { settings, updateSettings, wabUrl, useRemoteStorage, useMessageBox, storageUrl, useWab, messageBoxUrl, backupStorageUrls, addBackupStorageUrl, removeBackupStorageUrl, syncBackupStorage, permissionsConfig, updatePermissionsConfig } = useContext(WalletContext) + const { settings, updateSettings, wabUrl, useRemoteStorage, useMessageBox, storageUrl, useWab, messageBoxUrl, backupStorageUrls, addBackupStorageUrl, removeBackupStorageUrl, syncBackupStorage, setPrimaryStorage, permissionsConfig, updatePermissionsConfig } = useContext(WalletContext) const { pageLoaded, setManualUpdateInfo } = useContext(UserContext) const [settingsLoading, setSettingsLoading] = useState(false) const theme = useTheme() @@ -184,8 +184,9 @@ const Settings = () => { await addBackupStorageUrl(local ? 'LOCAL_STORAGE' : newBackupUrl); setShowBackupDialog(false); setNewBackupUrl(''); - } catch (e) { - // Error already shown by addBackupStorageUrl + } catch (e: any) { + console.error('[Settings] addBackupStorageUrl failed:', e); + toast.error(t('backup_error_add_failed', { message: e?.message || 'unknown error' })); } finally { setBackupLoading(false); } @@ -202,6 +203,27 @@ const Settings = () => { } } + const handleMakePrimary = async (target: string) => { + try { + setBackupLoading(true); + setSyncError(''); + setSyncProgressLogs([]); + setSyncComplete(false); + setShowSyncProgress(true); + const progressCallback = (message: string) => { + for (const line of message.split('\n')) { + if (line.trim()) setSyncProgressLogs((prev) => [...prev, line]); + } + }; + await setPrimaryStorage(target, progressCallback); + setSyncComplete(true); + } catch (e: any) { + setSyncError(e?.message || 'Failed to switch primary storage'); + } finally { + setBackupLoading(false); + } + } + const handleSyncBackupStorage = async () => { // Reset state setSyncError(''); @@ -571,6 +593,15 @@ const Settings = () => { }}> {url === 'LOCAL_STORAGE' ? t('backup_storage_local_electron') : url} +