From d9c01a5d07c2ce44064c2e9f2191ab31e93995ad Mon Sep 17 00:00:00 2001 From: Blazebrain Date: Thu, 17 Apr 2025 07:51:03 +0100 Subject: [PATCH 1/6] fix(desktop-pin-code-issue): persist FocusNode so KeyboardListener works on macOS Previously, every rebuild created a new FocusNode, so KeyboardListener never held focus and missed key events on macOS. This change: - Moves the FocusNode into state and initializes it in initState - Requests focus once after the first frame - Disposes of the FocusNode in dispose - Removes the inline FocusNode creation from build --- lib/src/screens/pin_code/pin_code_widget.dart | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index f6249576d..410349336 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -38,6 +38,7 @@ class PinCodeState extends State { static const fourPinLength = 4; final _gridViewKey = GlobalKey(); final _key = GlobalKey(); + late final FocusNode _focusNode; int pinLength; String pin; @@ -54,7 +55,17 @@ class PinCodeState extends State { pin = ''; title = S.current.enter_your_pin; _aspectRatio = 0; - WidgetsBinding.instance.addPostFrameCallback(_afterLayout); + _focusNode = FocusNode(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + _afterLayout(_); + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); } void setTitle(String title) => setState(() => this.title = title); @@ -120,8 +131,8 @@ class PinCodeState extends State { ); return KeyboardListener( - focusNode: FocusNode(), - autofocus: true, + focusNode: _focusNode, + autofocus: false, onKeyEvent: (keyEvent) { if (keyEvent is KeyDownEvent) { if (keyEvent.logicalKey.keyLabel == "Backspace") { @@ -144,8 +155,7 @@ class PinCodeState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.titleColor)), + color: Theme.of(context).extension()!.titleColor)), Spacer(flex: 8), Container( width: 180, @@ -162,7 +172,9 @@ class PinCodeState extends State { shape: BoxShape.circle, color: isFilled ? Theme.of(context).extension()!.titleColor - : Theme.of(context).extension()!.indicatorsColor + : Theme.of(context) + .extension()! + .indicatorsColor .withOpacity(0.25), )); }), @@ -225,7 +237,8 @@ class PinCodeState extends State { child: TextButton( onPressed: () => _pop(), style: TextButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: + Theme.of(context).colorScheme.background, shape: CircleBorder(), ), child: deleteIconImage, @@ -250,7 +263,9 @@ class PinCodeState extends State { style: TextStyle( fontSize: 25.0, fontWeight: FontWeight.w600, - color: Theme.of(context).extension()!.titleColor)), + color: Theme.of(context) + .extension()! + .titleColor)), ), ); }), From fe435d4e3bd40ac2fd51e83b9db15f5b807e6fae Mon Sep 17 00:00:00 2001 From: Blazebrain Date: Thu, 17 Apr 2025 08:09:53 +0100 Subject: [PATCH 2/6] fix(buy/sell-flow): add buy/sell toggle to DesktopExchangeCardsSection The desktop exchange screen was only showing the buy flow with no way to switch to sell. This change adds the missing toggle to the DesktopExchangeCardsSection and links it up in the buy/sell page --- lib/src/screens/buy/buy_sell_page.dart | 8 ++++++ .../desktop_exchange_cards_section.dart | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart index 48334f439..df81c60ce 100644 --- a/lib/src/screens/buy/buy_sell_page.dart +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -490,11 +490,19 @@ class BuySellPage extends BasePage { return DesktopExchangeCardsSection( firstExchangeCard: fiatExchangeCard, secondExchangeCard: cryptoExchangeCard, + onBuyTap: () => null, + onSellTap: () => + buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + isBuySellOption: true, ); } else { return DesktopExchangeCardsSection( firstExchangeCard: cryptoExchangeCard, secondExchangeCard: fiatExchangeCard, + onBuyTap: () => + !buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null, + onSellTap: () => null, + isBuySellOption: true, ); } }, diff --git a/lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart b/lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart index 0a97d7bad..5bc07091b 100644 --- a/lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart +++ b/lib/src/screens/exchange/widgets/desktop_exchange_cards_section.dart @@ -1,15 +1,22 @@ +import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; import 'package:flutter/material.dart'; class DesktopExchangeCardsSection extends StatelessWidget { - final Widget firstExchangeCard; - final Widget secondExchangeCard; - const DesktopExchangeCardsSection({ Key? key, required this.firstExchangeCard, required this.secondExchangeCard, + this.isBuySellOption = false, + this.onBuyTap, + this.onSellTap, }) : super(key: key); + final Widget firstExchangeCard; + final Widget secondExchangeCard; + final bool isBuySellOption; + final VoidCallback? onBuyTap; + final VoidCallback? onSellTap; + @override Widget build(BuildContext context) { return FocusTraversalGroup( @@ -18,7 +25,18 @@ class DesktopExchangeCardsSection extends StatelessWidget { children: [ Padding( padding: EdgeInsets.only(top: 55, left: 24, right: 24), - child: firstExchangeCard, + child: Column( + children: [ + if (isBuySellOption) + Column( + children: [ + const SizedBox(height: 16), + BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap), + ], + ), + firstExchangeCard, + ], + ), ), Padding( padding: EdgeInsets.only(top: 29, left: 24, right: 24), From ffe1c115fab2154c7eed4a13a3eb7367db1c24c8 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 18 Apr 2025 15:53:22 +0300 Subject: [PATCH 3/6] Cw 1038 filter transaction popup not scrollable (#2207) * ui:make overflowing filter sections scrollable * Update pull_request_template.md --- .github/pull_request_template.md | 1 + .../dashboard/widgets/filter_widget.dart | 245 +++++++++++------- lib/src/widgets/alert_close_button.dart | 30 +-- 3 files changed, 164 insertions(+), 112 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 18ad16e4b..272f7bbee 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -11,3 +11,4 @@ Please include a summary of the changes and which issue is fixed / feature is ad - [ ] Format code - [ ] Look for code duplication - [ ] Clear naming for variables and methods +- [ ] Manual tests in accessibility mode (TalkBack on Android) passed diff --git a/lib/src/screens/dashboard/widgets/filter_widget.dart b/lib/src/screens/dashboard/widgets/filter_widget.dart index eaf00a1de..81f29b81c 100644 --- a/lib/src/screens/dashboard/widgets/filter_widget.dart +++ b/lib/src/screens/dashboard/widgets/filter_widget.dart @@ -1,123 +1,178 @@ +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/filter_tile.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; -import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item.dart'; -import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; - -//import 'package:date_range_picker/date_range_picker.dart' as date_rage_picker; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; -class FilterWidget extends StatelessWidget { - FilterWidget({required this.filterItems}); +class FilterWidget extends StatefulWidget { + const FilterWidget({required this.filterItems, this.onClose, Key? key}) : super(key: key); final Map> filterItems; + final Function()? onClose; + + @override + _FilterWidgetState createState() => _FilterWidgetState(); +} + +class _FilterWidgetState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - const sectionDivider = const HorizontalSectionDivider(); - return PickerWrapperWidget( - children: [ - Padding( - padding: EdgeInsets.only(left: 24, right: 24, top: 24), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(24)), - child: Container( - color: Theme.of(context).extension()!.backgroundColor, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.all(24.0), - child: Text( - S.of(context).filter_by, - style: TextStyle( - color: - Theme.of(context).extension()!.detailsTitlesColor, - fontSize: 16, - fontFamily: 'Lato', - decoration: TextDecoration.none, - ), - ), - ), - sectionDivider, - ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: filterItems.length, - separatorBuilder: (context, _) => sectionDivider, - itemBuilder: (_, index1) { - final title = filterItems.keys.elementAt(index1); - final section = filterItems.values.elementAt(index1); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(top: 20, left: 24, right: 24), - child: Text( - title, - style: TextStyle( - color: Theme.of(context).extension()!.titleColor, - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.bold, - decoration: TextDecoration.none), - ), + return AlertBackground( + child: Column( + children: [ + const Expanded(child: SizedBox()), + Expanded( + flex: responsiveLayoutUtil.shouldRenderTabletUI ? 16 : 8, + child: LayoutBuilder( + builder: (context, constraints) { + double availableHeight = constraints.maxHeight; + return _buildFilterContent(context, availableHeight); + }, + ), + ), + Expanded( + child: AlertCloseButton( + key: const ValueKey('filter_wrapper_close_button_key'), + isPositioned: false, + onTap: widget.onClose, + ), + ), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _buildFilterContent(BuildContext context, double availableHeight) { + const sectionDivider = HorizontalSectionDivider(); + + const double totalHeaderHeight = 73; + const double filterTileMinHeight = 40; + double availableHeightForItems = availableHeight - totalHeaderHeight; + + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 24, right: 24, top: 24), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Container( + color: Theme.of(context).extension()!.backgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(24.0), + child: Text( + S.of(context).filter_by, + style: TextStyle( + color: Theme.of(context) + .extension()! + .detailsTitlesColor, + fontSize: 16, + fontFamily: 'Lato', + decoration: TextDecoration.none, ), - ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 28.0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + ), + ), + sectionDivider, + ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: widget.filterItems.length, + separatorBuilder: (context, _) => sectionDivider, + itemBuilder: (_, index1) { + final title = widget.filterItems.keys.elementAt(index1); + final section = widget.filterItems.values.elementAt(index1); + + final double itemHeight = + availableHeightForItems / widget.filterItems.length; + + final isSectionScrollable = + (itemHeight < (section.length * filterTileMinHeight)); + + final Widget sectionListView = ListView.builder( + controller: isSectionScrollable ? _scrollController : null, + padding: const EdgeInsets.symmetric(horizontal: 28.0), + shrinkWrap: isSectionScrollable ? false : true, + physics: isSectionScrollable + ? const BouncingScrollPhysics() + : const NeverScrollableScrollPhysics(), itemCount: section.length, itemBuilder: (_, index2) { final item = section[index2]; - - if (item is DropdownFilterItem) { - return Padding( - padding: EdgeInsets.fromLTRB(8, 0, 8, 16), - child: Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.0, - color: Theme.of(context).extension()!.secondaryTextColor), - ), - ), - child: DropdownFilterList( - items: item.items, - caption: item.caption, - selectedItem: item.selectedItem, - onItemSelected: item.onItemSelected, - ), - ), - ); - } final content = Observer( - builder: (_) => StandardCheckbox( - value: item.value(), - caption: item.caption, - gradientBackground: true, - borderColor: Theme.of(context).dividerColor, - iconColor: Colors.white, - onChanged: (value) => item.onChanged(), - )); - return FilterTile(child: content); + builder: (_) => StandardCheckbox( + value: item.value(), + caption: item.caption, + gradientBackground: true, + borderColor: Theme.of(context).dividerColor, + iconColor: Colors.white, + onChanged: (value) => item.onChanged(), + ), + ); + return FilterTile( + child: content, + ); }, - ) - ], - ); - }, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 20, left: 24, right: 24), + child: Text( + title, + style: TextStyle( + color: Theme.of(context).extension()!.titleColor, + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + Container( + height: isSectionScrollable ? itemHeight - totalHeaderHeight : null, + child: isSectionScrollable + ? Scrollbar( + controller: _scrollController, + thumbVisibility: true, + child: sectionListView, + ) + : sectionListView, + ), + ], + ); + }, + ), + ], ), - ]), + ), ), ), - ) - ], + ], + ), ); } } diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index 6ef0bdaa5..30e37ef20 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -7,6 +7,7 @@ class AlertCloseButton extends StatelessWidget { this.image, this.bottom, this.onTap, + this.isPositioned = true, super.key, }); @@ -14,6 +15,7 @@ class AlertCloseButton extends StatelessWidget { final Image? image; final double? bottom; + final bool isPositioned; final closeButton = Image.asset( 'assets/images/close.png', @@ -22,24 +24,18 @@ class AlertCloseButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Positioned( - bottom: bottom ?? 60, - child: GestureDetector( + final button = GestureDetector( onTap: onTap ?? () => Navigator.of(context).pop(), child: Semantics( - label: S.of(context).close, - button: true, - enabled: true, - child: Container( - height: 42, - width: 42, - decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), - child: Center( - child: image ?? closeButton, - ), - ), - ), - ), - ); + label: S.of(context).close, + button: true, + enabled: true, + child: Container( + height: 42, + width: 42, + decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), + child: Center(child: image ?? closeButton)))); + + return isPositioned ? Positioned(bottom: bottom ?? 60, child: button) : button; } } From 53efdced6861d967f6271ef41c631e4fafc7611a Mon Sep 17 00:00:00 2001 From: cyan Date: Sat, 19 Apr 2025 01:13:07 +0200 Subject: [PATCH 4/6] fix: more verbose errors in backups (#2196) * fix: more verbose errors in backups * show user friendly error, but prompt to send the full logs [skip ci] --------- Co-authored-by: Omar Hatem --- lib/view_model/restore_from_backup_view_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view_model/restore_from_backup_view_model.dart b/lib/view_model/restore_from_backup_view_model.dart index a74beeaea..388782cb0 100644 --- a/lib/view_model/restore_from_backup_view_model.dart +++ b/lib/view_model/restore_from_backup_view_model.dart @@ -50,7 +50,7 @@ abstract class RestoreFromBackupViewModelBase with Store { state = FailureState('This is not a valid backup file, please make sure you selected the correct backup file'); } else { state = FailureState('Failed to restore backup, please try again'); - ExceptionHandler.onError(FlutterErrorDetails(exception: e, stack: s, silent: true)); + ExceptionHandler.onError(FlutterErrorDetails(exception: e, stack: s)); } } From e09d9aadfa58be8fc734800bbfec1cb64fb3b72b Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Sat, 19 Apr 2025 01:13:37 +0200 Subject: [PATCH 5/6] - Improve code for backup cache (#2216) * - Improve code for backup cache - Backup all Cached values without manually adding each one * Exclude some cache keys from backup [skip ci] * Exclude some cache keys from backup [skip ci] --- lib/core/backup_service.dart | 241 ++++++----------------------------- 1 file changed, 37 insertions(+), 204 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 2af037d44..94599d5f2 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -1,6 +1,5 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/get_encryption_key.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -164,6 +163,31 @@ class $BackupService { } final data = json.decode(preferencesFile.readAsStringSync()) as Map; + + try { // shouldn't throw an error but just in case, so it doesn't stop the backup restore + for (var entry in data.entries) { + String key = entry.key; + dynamic value = entry.value; + + // Check the type of the value and save accordingly + if (value is String) { + await sharedPreferences.setString(key, value); + } else if (value is int) { + await sharedPreferences.setInt(key, value); + } else if (value is double) { + await sharedPreferences.setDouble(key, value); + } else if (value is bool) { + await sharedPreferences.setBool(key, value); + } else if (value is List) { + await sharedPreferences.setStringList(key, value); + } else { + if (kDebugMode) { + printV('Skipping individual save for key "$key": Unsupported type (${value.runtimeType}). Value: $value'); + } + } + } + } catch (_) {} + String currentWalletName = data[PreferencesKey.currentWalletName] as String; int currentWalletType = data[PreferencesKey.currentWalletType] as int; @@ -175,151 +199,10 @@ class $BackupService { currentWalletType = serializeToInt(correctWallets.first.type); } - final currentNodeId = data[PreferencesKey.currentNodeIdKey] as int?; - final currentBalanceDisplayMode = data[PreferencesKey.currentBalanceDisplayModeKey] as int?; - final currentFiatCurrency = data[PreferencesKey.currentFiatCurrencyKey] as String?; - final shouldSaveRecipientAddress = data[PreferencesKey.shouldSaveRecipientAddressKey] as bool?; - final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; - final disableTradeOption = data[PreferencesKey.disableTradeOption] as bool?; - final currentTransactionPriorityKeyLegacy = - data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; - final currentBitcoinElectrumSererId = - data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; - final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; - final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?; - final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?; - final currentTheme = data[PreferencesKey.currentTheme] as int?; - final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?; - final currentDefaultSettingsMigrationVersion = - data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; - final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; - final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; - final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; - final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; - final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; - final defaultNanoRep = data[PreferencesKey.defaultNanoRep] as String?; - final defaultBananoRep = data[PreferencesKey.defaultBananoRep] as String?; - final lookupsTwitter = data[PreferencesKey.lookupsTwitter] as bool?; - final lookupsMastodon = data[PreferencesKey.lookupsMastodon] as bool?; - final lookupsYatService = data[PreferencesKey.lookupsYatService] as bool?; - final lookupsUnstoppableDomains = data[PreferencesKey.lookupsUnstoppableDomains] as bool?; - final lookupsOpenAlias = data[PreferencesKey.lookupsOpenAlias] as bool?; - final lookupsENS = data[PreferencesKey.lookupsENS] as bool?; - final lookupsWellKnown = data[PreferencesKey.lookupsWellKnown] as bool?; - final syncAll = data[PreferencesKey.syncAllKey] as bool?; - final syncMode = data[PreferencesKey.syncModeKey] as int?; - final autoGenerateSubaddressStatus = - data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; - - await sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); - - if (currentNodeId != null) - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId); - - if (currentBalanceDisplayMode != null) - await sharedPreferences.setInt( - PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode); - - await sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); - - if (currentFiatCurrency != null) - await sharedPreferences.setString( - PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency); - - if (shouldSaveRecipientAddress != null) - await sharedPreferences.setBool( - PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress); - - if (isAppSecure != null) - await sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); - - if (disableTradeOption != null) - await sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption); - - if (currentTransactionPriorityKeyLegacy != null) - await sharedPreferences.setInt( - PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); - - if (currentBitcoinElectrumSererId != null) - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); - - if (currentLanguageCode != null) - await sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode); - - if (displayActionListMode != null) - await sharedPreferences.setInt( - PreferencesKey.displayActionListModeKey, displayActionListMode); - - if (fiatApiMode != null) - await sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); - if (autoGenerateSubaddressStatus != null) - await sharedPreferences.setInt( - PreferencesKey.autoGenerateSubaddressStatusKey, autoGenerateSubaddressStatus); - - if (currentTheme != null && DeviceInfo.instance.isMobile) { - await sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); - // enforce dark theme on desktop platforms until the design is ready: - } else if (DeviceInfo.instance.isDesktop) { + if (DeviceInfo.instance.isDesktop) { await sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw); } - if (exchangeStatus != null) - await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); - - if (currentDefaultSettingsMigrationVersion != null) - await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, - currentDefaultSettingsMigrationVersion); - - if (moneroTransactionPriority != null) - await sharedPreferences.setInt( - PreferencesKey.moneroTransactionPriority, moneroTransactionPriority); - - if (bitcoinTransactionPriority != null) - await sharedPreferences.setInt( - PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); - - if (sortBalanceTokensBy != null) - await sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); - - if (pinNativeTokenAtTop != null) - await sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); - - if (useEtherscan != null) - await sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); - - if (defaultNanoRep != null) - await sharedPreferences.setString(PreferencesKey.defaultNanoRep, defaultNanoRep); - - if (defaultBananoRep != null) - await sharedPreferences.setString(PreferencesKey.defaultBananoRep, defaultBananoRep); - - if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); - if (lookupsTwitter != null) - await sharedPreferences.setBool(PreferencesKey.lookupsTwitter, lookupsTwitter); - - if (lookupsMastodon != null) - await sharedPreferences.setBool(PreferencesKey.lookupsMastodon, lookupsMastodon); - - if (lookupsYatService != null) - await sharedPreferences.setBool(PreferencesKey.lookupsYatService, lookupsYatService); - - if (lookupsUnstoppableDomains != null) - await sharedPreferences.setBool( - PreferencesKey.lookupsUnstoppableDomains, lookupsUnstoppableDomains); - - if (lookupsOpenAlias != null) - await sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, lookupsOpenAlias); - - if (lookupsENS != null) await sharedPreferences.setBool(PreferencesKey.lookupsENS, lookupsENS); - - if (lookupsWellKnown != null) - await sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, lookupsWellKnown); - - if (syncAll != null) await sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); - - if (syncMode != null) await sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); - await preferencesFile.delete(); } @@ -378,11 +261,6 @@ class $BackupService { await keyService.saveWalletPassword(walletName: name, password: password); } - @Deprecated('Use v2 instead') - Future _exportKeychainDumpV1(String password, - {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async => - throw Exception('Deprecated'); - Future exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); @@ -402,59 +280,18 @@ class $BackupService { return encrypted; } + static const List _excludedPrefsKeys = [ + PreferencesKey.currentPinLength, + PreferencesKey.showCameraConsent, + PreferencesKey.lastSeenAppVersion, + PreferencesKey.failedTotpTokenTrials, + ]; + Future exportPreferencesJSON() async { - final preferences = { - PreferencesKey.currentWalletName: - sharedPreferences.getString(PreferencesKey.currentWalletName), - PreferencesKey.currentNodeIdKey: sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), - PreferencesKey.currentBalanceDisplayModeKey: - sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey), - PreferencesKey.currentWalletType: sharedPreferences.getInt(PreferencesKey.currentWalletType), - PreferencesKey.currentFiatCurrencyKey: - sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), - PreferencesKey.shouldSaveRecipientAddressKey: - sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableTradeOption: sharedPreferences.getBool(PreferencesKey.disableTradeOption), - PreferencesKey.currentTransactionPriorityKeyLegacy: - sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), - PreferencesKey.currentBitcoinElectrumSererIdKey: - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), - PreferencesKey.currentLanguageCode: - sharedPreferences.getString(PreferencesKey.currentLanguageCode), - PreferencesKey.displayActionListModeKey: - sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), - PreferencesKey.currentTheme: sharedPreferences.getInt(PreferencesKey.currentTheme), - PreferencesKey.exchangeStatusKey: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), - PreferencesKey.currentDefaultSettingsMigrationVersion: - sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), - PreferencesKey.bitcoinTransactionPriority: - sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), - PreferencesKey.moneroTransactionPriority: - sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), - PreferencesKey.currentFiatApiModeKey: - sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), - PreferencesKey.sortBalanceBy: sharedPreferences.getInt(PreferencesKey.sortBalanceBy), - PreferencesKey.pinNativeTokenAtTop: - sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), - PreferencesKey.useEtherscan: sharedPreferences.getBool(PreferencesKey.useEtherscan), - PreferencesKey.defaultNanoRep: sharedPreferences.getString(PreferencesKey.defaultNanoRep), - PreferencesKey.defaultBananoRep: - sharedPreferences.getString(PreferencesKey.defaultBananoRep), - PreferencesKey.lookupsTwitter: sharedPreferences.getBool(PreferencesKey.lookupsTwitter), - PreferencesKey.lookupsMastodon: sharedPreferences.getBool(PreferencesKey.lookupsMastodon), - PreferencesKey.lookupsYatService: - sharedPreferences.getBool(PreferencesKey.lookupsYatService), - PreferencesKey.lookupsUnstoppableDomains: - sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains), - PreferencesKey.lookupsOpenAlias: sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias), - PreferencesKey.lookupsENS: sharedPreferences.getBool(PreferencesKey.lookupsENS), - PreferencesKey.lookupsWellKnown: - sharedPreferences.getBool(PreferencesKey.lookupsWellKnown), - PreferencesKey.syncModeKey: sharedPreferences.getInt(PreferencesKey.syncModeKey), - PreferencesKey.syncAllKey: sharedPreferences.getBool(PreferencesKey.syncAllKey), - PreferencesKey.autoGenerateSubaddressStatusKey: - sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), - }; + final preferences = {}; + sharedPreferences.getKeys().forEach((key) => preferences[key] = sharedPreferences.get(key)); + + _excludedPrefsKeys.forEach((key) => preferences.remove(key)); return json.encode(preferences); } @@ -466,10 +303,6 @@ class $BackupService { return Uint8List.fromList(bytes); } - @Deprecated('Use v2 instead') - Future _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async => - throw Exception('Deprecated'); - Future _decryptV1(Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async { final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); From 3e25be6dcf283219ceee852d056356ef2dde098a Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 22 Apr 2025 21:41:31 +0200 Subject: [PATCH 6/6] fix: allow bakcups to be created even if one of the wallets is corrupted (#2221) --- lib/core/backup_service.dart | 20 ++++++++++++++------ tool/configure.dart | 8 ++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 94599d5f2..385598060 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -265,16 +265,24 @@ class $BackupService { {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final wallets = await Future.wait(walletInfoSource.values.map((walletInfo) async { - return { - 'name': walletInfo.name, - 'type': walletInfo.type.toString(), - 'password': await keyService.getWalletPassword(walletName: walletInfo.name) - }; + try { + return { + 'name': walletInfo.name, + 'type': walletInfo.type.toString(), + 'password': await keyService.getWalletPassword(walletName: walletInfo.name) + }; + } catch (e) { + return { + 'name': walletInfo.name, + 'type': walletInfo.type.toString(), + 'password': '' + }; + } })); final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = await _secureStorage.read(key: backupPasswordKey); final data = utf8.encode( - json.encode({'wallets': wallets, backupPasswordKey: backupPassword})); + json.encode({'wallets': wallets, backupPasswordKey: backupPassword, '_all': await _secureStorage.readAll()})); final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password'); return encrypted; diff --git a/tool/configure.dart b/tool/configure.dart index 4711373b7..2a29195c6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1629,6 +1629,7 @@ abstract class SecureStorage { Future delete({required String key}); // Legacy Future readNoIOptions({required String key}); + Future> readAll(); }"""; const defaultSecureStorage = """ class DefaultSecureStorage extends SecureStorage { @@ -1667,6 +1668,11 @@ class DefaultSecureStorage extends SecureStorage { iOptions: useNoIOptions ? IOSOptions() : null, ); } + + @override + Future> readAll() async { + return await _secureStorage.readAll(); + } }"""; const fakeSecureStorage = """ class FakeSecureStorage extends SecureStorage { @@ -1678,6 +1684,8 @@ class FakeSecureStorage extends SecureStorage { Future delete({required String key}) async {} @override Future readNoIOptions({required String key}) async => null; + @override + Future> readAll() async => {}; }"""; final outputFile = File(secureStoragePath); final header = hasFlutterSecureStorage