diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index ebb6b94cf..2a08a49fe 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -121,7 +121,7 @@ class _RestoreFromFileViewState extends ConsumerState { await showDialog( context: context, barrierDismissible: false, - builder: (_) => !Util.isDesktop + builder: (dialogContext) => !Util.isDesktop ? StackOkDialog(title: "Backup saved to:", message: savedPath) : DesktopDialog( maxHeight: double.infinity, @@ -154,7 +154,9 @@ class _RestoreFromFileViewState extends ConsumerState { child: PrimaryButton( label: "Ok", buttonHeight: ButtonHeight.l, - onPressed: Navigator.of(context).pop, + onPressed: () { + Navigator.of(dialogContext).pop(); + }, ), ), ], @@ -176,6 +178,7 @@ class _RestoreFromFileViewState extends ConsumerState { builder: (_) => StackOkDialog( title: "Backup creation failed", message: ex?.toString() ?? "Unexpected error", + desktopPopRootNavigator: true, ), ); } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 5f93a9dea..4b05ab122 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -275,9 +275,32 @@ abstract class SWB { backupWallet['isFavorite'] = wallet.info.isFavourite; backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString; - if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { - backupWallet['viewOnlyWalletDataKey'] = - (await wallet.getViewOnlyWalletData()).toJsonEncodedString(); + // Check secure storage for view-only data even if flag is missing. + String? rawViewOnlyData; + if (wallet is ViewOnlyOptionInterface) { + rawViewOnlyData = await _secureStore.read( + key: Wallet.getViewOnlyWalletDataSecStoreKey( + walletId: wallet.walletId, + ), + ); + } + + if (rawViewOnlyData != null) { + backupWallet['viewOnlyWalletDataKey'] = rawViewOnlyData; + // Patch missing isViewOnlyKey flag in otherDataJsonString. + if (wallet.info.otherData[WalletInfoKeys.isViewOnlyKey] != true) { + final patchedOtherData = Map.from( + wallet.info.otherData, + ); + patchedOtherData[WalletInfoKeys.isViewOnlyKey] = true; + final parsed = ViewOnlyWalletData.fromJsonEncodedString( + rawViewOnlyData, + walletId: wallet.walletId, + ); + patchedOtherData[WalletInfoKeys.viewOnlyTypeIndexKey] = + parsed.type.index; + backupWallet['otherDataJsonString'] = jsonEncode(patchedOtherData); + } } else if (wallet is MnemonicInterface) { backupWallet['mnemonic'] = await wallet.getMnemonic(); backupWallet['mnemonicPassphrase'] = await wallet @@ -378,7 +401,7 @@ abstract class SWB { String? mnemonic, mnemonicPassphrase, privateKey; ViewOnlyWalletData? viewOnlyData; - if (info.isViewOnly) { + if (info.isViewOnly || walletbackup['viewOnlyWalletDataKey'] is String) { final viewOnlyDataEncoded = walletbackup['viewOnlyWalletDataKey'] as String; @@ -775,6 +798,28 @@ abstract class SWB { ); } + // Patch missing isViewOnlyKey if backup has view-only data. + if (walletbackup['viewOnlyWalletDataKey'] is String && + otherData?[WalletInfoKeys.isViewOnlyKey] != true) { + otherData ??= {}; + otherData[WalletInfoKeys.isViewOnlyKey] = true; + if (otherData[WalletInfoKeys.viewOnlyTypeIndexKey] == null) { + try { + final parsed = ViewOnlyWalletData.fromJsonEncodedString( + walletbackup['viewOnlyWalletDataKey'] as String, + walletId: walletId, + ); + otherData[WalletInfoKeys.viewOnlyTypeIndexKey] = parsed.type.index; + } catch (e, s) { + Logging.instance.e( + "SWB restore: failed to recover viewOnlyTypeIndexKey", + error: e, + stackTrace: s, + ); + } + } + } + final info = WalletInfo( coinName: coin.identifier, walletId: walletId, diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart index 39537e37d..68fab92fe 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -42,6 +42,7 @@ Future showLoading({ unawaited( showDialog( context: context, + useRootNavigator: rootNavigator, barrierDismissible: false, builder: (_) => WillPopScope( onWillPop: () async => false, diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index db0a65c70..2d1d76fd2 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -187,60 +187,60 @@ class WalletInfo implements IsarId { required Balance newBalance, required Isar isar, }) async { - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - final newEncoded = newBalance.toJsonIgnoreCoin(); - - // only update if there were changes to the balance - if (thisInfo.cachedBalanceString != newEncoded) { - await isar.writeTxn(() async { + // Read inside the tx so concurrent updates don't create a race condition. + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && thisInfo.cachedBalanceString != newEncoded) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(cachedBalanceString: newEncoded), ); - }); - } + } + }); } Future updateBalanceSecondary({ required Balance newBalance, required Isar isar, }) async { - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - final newEncoded = newBalance.toJsonIgnoreCoin(); - - // only update if there were changes to the balance - if (thisInfo.cachedBalanceSecondaryString != newEncoded) { - await isar.writeTxn(() async { + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && + thisInfo.cachedBalanceSecondaryString != newEncoded) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(cachedBalanceSecondaryString: newEncoded), ); - }); - } + } + }); } Future updateBalanceTertiary({ required Balance newBalance, required Isar isar, }) async { - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - final newEncoded = newBalance.toJsonIgnoreCoin(); - - // only update if there were changes to the balance - if (thisInfo.cachedBalanceTertiaryString != newEncoded) { - await isar.writeTxn(() async { + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && + thisInfo.cachedBalanceTertiaryString != newEncoded) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(cachedBalanceTertiaryString: newEncoded), ); - }); - } + } + }); } /// copies this with a new chain height and updates the db @@ -248,17 +248,18 @@ class WalletInfo implements IsarId { required int newHeight, required Isar isar, }) async { - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - // only update if there were changes to the height - if (thisInfo.cachedChainHeight != newHeight) { - await isar.writeTxn(() async { + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && thisInfo.cachedChainHeight != newHeight) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(cachedChainHeight: newHeight), ); - }); - } + } + }); } /// update favourite wallet and its index it the ui list. @@ -283,18 +284,18 @@ class WalletInfo implements IsarId { index = -1; } - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - - // only update if there were changes to the height - if (thisInfo.favouriteOrderIndex != index) { - await isar.writeTxn(() async { + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && thisInfo.favouriteOrderIndex != index) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(favouriteOrderIndex: index), ); - }); - } + } + }); } /// copies this with a new name and updates the db @@ -303,35 +304,35 @@ class WalletInfo implements IsarId { if (newName.isEmpty) { throw Exception("Empty wallet name not allowed!"); } - - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - - // only update if there were changes to the name - if (thisInfo.name != newName) { - await isar.writeTxn(() async { + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && thisInfo.name != newName) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put(thisInfo.copyWith(name: newName)); - }); - } + } + }); } - /// copies this with a new name and updates the db + /// copies this with a new receiving address and updates the db Future updateReceivingAddress({ required String newAddress, required Isar isar, }) async { - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - // only update if there were changes to the name - if (thisInfo.cachedReceivingAddress != newAddress) { - await isar.writeTxn(() async { + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && thisInfo.cachedReceivingAddress != newAddress) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(cachedReceivingAddress: newAddress), ); - }); - } + } + }); } /// update [otherData] with the map entries in [newEntries] @@ -339,23 +340,24 @@ class WalletInfo implements IsarId { required Map newEntries, required Isar isar, }) async { - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo == null) return; - final Map newMap = {}; - newMap.addAll(thisInfo.otherData); - newMap.addAll(newEntries); - final encodedNew = jsonEncode(newMap); + final newMap = Map.from(thisInfo.otherData) + ..addAll(newEntries); + final encodedNew = jsonEncode(newMap); - // only update if there were changes - if (thisInfo.otherDataJsonString != encodedNew) { - await isar.writeTxn(() async { + if (thisInfo.otherDataJsonString != encodedNew) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(otherDataJsonString: encodedNew), ); - }); - } + } + }); } /// Can be dangerous. Don't use unless you know the consequences @@ -385,28 +387,26 @@ class WalletInfo implements IsarId { } } - /// copies this with a new name and updates the db + /// copies this with a new restore height and updates the db Future updateRestoreHeight({ required int newRestoreHeight, required Isar isar, }) async { - // don't allow empty names if (newRestoreHeight < 0) { throw Exception("Negative restore height not allowed!"); } - - // try to get latest instance of this from db - final thisInfo = await isar.walletInfo.get(id) ?? this; - - // only update if there were changes to the name - if (thisInfo.restoreHeight != newRestoreHeight) { - await isar.writeTxn(() async { + await isar.writeTxn(() async { + final thisInfo = await isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); + if (thisInfo != null && thisInfo.restoreHeight != newRestoreHeight) { await isar.walletInfo.delete(thisInfo.id); await isar.walletInfo.put( thisInfo.copyWith(restoreHeight: newRestoreHeight), ); - }); - } + } + }); } /// copies this with a new name and updates the db @@ -442,7 +442,8 @@ class WalletInfo implements IsarId { }) async { await updateOtherData( newEntries: { - WalletInfoKeys.solanaCustomTokenMintAddresses: newMintAddresses.toList(), + WalletInfoKeys.solanaCustomTokenMintAddresses: newMintAddresses + .toList(), }, isar: isar, );