From a3b9ceb33b0e81bc0261db892e6238f9a7a27610 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 6 Oct 2026 21:34:13 -0500 Subject: [PATCH 1/3] style: dart format the files touched by auto backup triggers CI's format check runs over every .dart file a PR touches, so reformat them up front instead of mixing the reflow into the feature diff. --- .../edit_auto_backup_view.dart | 77 ++-- .../backup_frequency_type_select_sheet.dart | 28 +- .../create_auto_backup.dart | 60 ++-- .../global/address_book_service_provider.dart | 6 +- .../global/auto_swb_service_provider.dart | 6 +- lib/services/address_book_service.dart | 1 + .../enums/backup_frequency_type.dart | 6 +- lib/utilities/format.dart | 5 +- lib/utilities/prefs.dart | 335 ++++++++---------- 9 files changed, 230 insertions(+), 294 deletions(-) diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 077ff21c51..f21a7bdd56 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -170,9 +170,8 @@ class _EditAutoBackupViewState extends ConsumerState { passwordRepeatController.text = ""; if (!Util.isDesktop) { - Navigator.of( - context, - ).popUntil(ModalRoute.withName(AutoBackupView.routeName)); + Navigator.of(context) + .popUntil(ModalRoute.withName(AutoBackupView.routeName)); } } } else { @@ -243,9 +242,9 @@ class _EditAutoBackupViewState extends ConsumerState { condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () { @@ -325,9 +324,9 @@ class _EditAutoBackupViewState extends ConsumerState { const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -394,9 +393,9 @@ class _EditAutoBackupViewState extends ConsumerState { hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -473,15 +472,15 @@ class _EditAutoBackupViewState extends ConsumerState { fillColor: passwordStrength < 0.51 ? Theme.of(context).extension()!.accentColorRed : passwordStrength < 1 - ? Theme.of( - context, - ).extension()!.accentColorYellow - : Theme.of( - context, - ).extension()!.accentColorGreen, - backgroundColor: Theme.of( - context, - ).extension()!.buttonBackSecondary, + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension()! + .buttonBackSecondary, percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, ), ), @@ -524,9 +523,9 @@ class _EditAutoBackupViewState extends ConsumerState { hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -547,9 +546,9 @@ class _EditAutoBackupViewState extends ConsumerState { "Auto Backup frequency", style: isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, ) : STextStyles.smallMed12(context), ), @@ -604,18 +603,18 @@ class _EditAutoBackupViewState extends ConsumerState { Assets.svg.chevronDown, width: 10, height: 5, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, ), ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -637,9 +636,9 @@ class _EditAutoBackupViewState extends ConsumerState { ), Positioned.fill( child: RawMaterialButton( - splashColor: Theme.of( - context, - ).extension()!.highlight, + splashColor: Theme.of(context) + .extension()! + .highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -676,9 +675,9 @@ class _EditAutoBackupViewState extends ConsumerState { padding: const EdgeInsets.only(right: 4.0), child: SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of( - context, - ).extension()!.textSubtitle2, + color: Theme.of(context) + .extension()! + .textSubtitle2, width: 12, height: 6, ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart index d61c93c626..8d578a5eb3 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart @@ -18,9 +18,7 @@ import '../../../../../utilities/enums/backup_frequency_type.dart'; import '../../../../../utilities/text_styles.dart'; class BackupFrequencyTypeSelectSheet extends ConsumerWidget { - const BackupFrequencyTypeSelectSheet({ - super.key, - }); + const BackupFrequencyTypeSelectSheet({super.key}); String prettyFrequencyType(BackupFrequencyType type) { switch (type) { @@ -44,9 +42,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { child: Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( padding: const EdgeInsets.only( @@ -73,9 +69,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -84,9 +78,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { style: STextStyles.pageTitleH2(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), for (int i = 0; i < BackupFrequencyType.values.length; i++) Column( children: [ @@ -131,9 +123,7 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { }, ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( children: [ @@ -151,14 +141,10 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ], ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), ], ), ], diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index 1a0fc67533..070ca48f16 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -349,9 +349,9 @@ class _CreateAutoBackup extends ConsumerState { const SizedBox(width: 16), SvgPicture.asset( Assets.svg.folder, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -383,9 +383,9 @@ class _CreateAutoBackup extends ConsumerState { "Create a passphrase", style: STextStyles.desktopTextExtraSmall(context) .copyWith( - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, ), textAlign: TextAlign.left, ), @@ -428,9 +428,9 @@ class _CreateAutoBackup extends ConsumerState { hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -507,19 +507,19 @@ class _CreateAutoBackup extends ConsumerState { width: 512, height: 5, fillColor: passwordStrength < 0.51 - ? Theme.of( - context, - ).extension()!.accentColorRed + ? Theme.of(context) + .extension()! + .accentColorRed : passwordStrength < 1 - ? Theme.of( - context, - ).extension()!.accentColorYellow - : Theme.of( - context, - ).extension()!.accentColorGreen, - backgroundColor: Theme.of( - context, - ).extension()!.buttonBackSecondary, + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + backgroundColor: Theme.of(context) + .extension()! + .buttonBackSecondary, percent: passwordStrength < 0.25 ? 0.03 : passwordStrength, @@ -562,9 +562,9 @@ class _CreateAutoBackup extends ConsumerState { hidePassword ? Assets.svg.eye : Assets.svg.eyeSlash, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, width: 16, height: 16, ), @@ -650,18 +650,18 @@ class _CreateAutoBackup extends ConsumerState { Assets.svg.chevronDown, width: 10, height: 5, - color: Theme.of( - context, - ).extension()!.textDark3, + color: Theme.of(context) + .extension()! + .textDark3, ), ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of( - context, - ).extension()!.textFieldDefaultBG, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), diff --git a/lib/providers/global/address_book_service_provider.dart b/lib/providers/global/address_book_service_provider.dart index e61f6960ae..c948360601 100644 --- a/lib/providers/global/address_book_service_provider.dart +++ b/lib/providers/global/address_book_service_provider.dart @@ -9,7 +9,9 @@ */ import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../services/address_book_service.dart'; -final addressBookServiceProvider = - ChangeNotifierProvider((ref) => AddressBookService()); +final addressBookServiceProvider = ChangeNotifierProvider( + (ref) => AddressBookService(), +); diff --git a/lib/providers/global/auto_swb_service_provider.dart b/lib/providers/global/auto_swb_service_provider.dart index ba67fef945..5310ee3ce0 100644 --- a/lib/providers/global/auto_swb_service_provider.dart +++ b/lib/providers/global/auto_swb_service_provider.dart @@ -9,11 +9,11 @@ */ import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'secure_store_provider.dart'; import '../../services/auto_swb_service.dart'; final autoSWBServiceProvider = ChangeNotifierProvider( - (ref) => AutoSWBService( - secureStorageInterface: ref.read(secureStoreProvider), - ), + (ref) => + AutoSWBService(secureStorageInterface: ref.read(secureStoreProvider)), ); diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index f49d9e6649..17381a6bec 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -10,6 +10,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; + import '../db/isar/main_db.dart'; import '../models/isar/models/contact_entry.dart'; diff --git a/lib/utilities/enums/backup_frequency_type.dart b/lib/utilities/enums/backup_frequency_type.dart index daec5df4e6..6a0992ff36 100644 --- a/lib/utilities/enums/backup_frequency_type.dart +++ b/lib/utilities/enums/backup_frequency_type.dart @@ -8,8 +8,4 @@ * */ -enum BackupFrequencyType { - everyTenMinutes, - everyAppStart, - afterClosingAWallet, -} +enum BackupFrequencyType { everyTenMinutes, everyAppStart, afterClosingAWallet } diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index 7a9fe1b69f..79ec8aecb6 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -68,8 +68,9 @@ abstract class Format { return dayAndYear; } - final minutes = - date.minute < 10 ? "0${date.minute}" : date.minute.toString(); + final minutes = date.minute < 10 + ? "0${date.minute}" + : date.minute.toString(); return "$dayAndYear, ${date.hour}:$minutes"; } diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 56013d64d7..36677bd6d6 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -108,10 +108,9 @@ class Prefs extends ChangeNotifier { Future _getLastUnlockedTimeout() async { return (DB.instance.get( - boxName: DB.boxNamePrefs, - key: "lastUnlockedTimeout", - )) - as int? ?? + boxName: DB.boxNamePrefs, + key: "lastUnlockedTimeout", + )) as int? ?? 60; } @@ -135,10 +134,9 @@ class Prefs extends ChangeNotifier { Future _getLastUnlocked() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "lastUnlocked", - ) - as int? ?? + boxName: DB.boxNamePrefs, + key: "lastUnlocked", + ) as int? ?? 0; } @@ -171,10 +169,9 @@ class Prefs extends ChangeNotifier { Future _getCurrentNotificationIndex() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "currentNotificationId", - ) - as int? ?? + boxName: DB.boxNamePrefs, + key: "currentNotificationId", + ) as int? ?? 0; } @@ -199,10 +196,9 @@ class Prefs extends ChangeNotifier { Future> _getWalletIdsSyncOnStartup() async { final list = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "walletIdsSyncOnStartup", - ) - as List? ?? + boxName: DB.boxNamePrefs, + key: "walletIdsSyncOnStartup", + ) as List? ?? []; return List.from(list); } @@ -228,10 +224,9 @@ class Prefs extends ChangeNotifier { Future _getSyncType() async { final int index = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "syncTypeIndex", - ) - as int? ?? + boxName: DB.boxNamePrefs, + key: "syncTypeIndex", + ) as int? ?? SyncingType.allWalletsOnStartup.index; return SyncingType.values[index]; } @@ -256,10 +251,9 @@ class Prefs extends ChangeNotifier { Future _getUseWifiOnly() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "wifiOnly", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "wifiOnly", + ) as bool? ?? false; } @@ -283,10 +277,9 @@ class Prefs extends ChangeNotifier { Future _getShowFavoriteWallets() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showFavoriteWallets", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "showFavoriteWallets", + ) as bool? ?? true; } @@ -310,10 +303,9 @@ class Prefs extends ChangeNotifier { Future _getPreferredLanguage() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "language", - ) - as String? ?? + boxName: DB.boxNamePrefs, + key: "language", + ) as String? ?? Language.englishUS.description; } @@ -337,10 +329,9 @@ class Prefs extends ChangeNotifier { Future _getPreferredCurrency() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "currency", - ) - as String? ?? + boxName: DB.boxNamePrefs, + key: "currency", + ) as String? ?? "USD"; } @@ -405,10 +396,9 @@ class Prefs extends ChangeNotifier { Future _getRandomizePIN() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "randomizePIN", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "randomizePIN", + ) as bool? ?? false; } @@ -432,10 +422,9 @@ class Prefs extends ChangeNotifier { Future _getUseBiometrics() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "useBiometrics", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "useBiometrics", + ) as bool? ?? false; } @@ -459,10 +448,9 @@ class Prefs extends ChangeNotifier { Future _getHasPin() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "hasPin", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "hasPin", + ) as bool? ?? false; } @@ -486,10 +474,9 @@ class Prefs extends ChangeNotifier { Future _getHasDuressPin() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "hasDuressPin", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "hasDuressPin", + ) as bool? ?? false; } @@ -513,10 +500,9 @@ class Prefs extends ChangeNotifier { Future _getBiometricsDuress() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "biometricsDuress", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "biometricsDuress", + ) as bool? ?? false; } @@ -540,10 +526,9 @@ class Prefs extends ChangeNotifier { Future _getHasFamiliarity() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "familiarity", - ) - as int? ?? + boxName: DB.boxNamePrefs, + key: "familiarity", + ) as int? ?? 0; } @@ -567,10 +552,9 @@ class Prefs extends ChangeNotifier { Future _getTorKillswitch() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "torKillswitch", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "torKillswitch", + ) as bool? ?? true; } @@ -594,10 +578,9 @@ class Prefs extends ChangeNotifier { Future _getShowTestNetCoins() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showTestNetCoins", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "showTestNetCoins", + ) as bool? ?? false; } @@ -624,10 +607,9 @@ class Prefs extends ChangeNotifier { Future _getIsAutoBackupEnabled() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "isAutoBackupEnabled", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "isAutoBackupEnabled", + ) as bool? ?? false; } @@ -651,10 +633,9 @@ class Prefs extends ChangeNotifier { Future _getAutoBackupLocation() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoBackupLocation", - ) - as String?; + boxName: DB.boxNamePrefs, + key: "autoBackupLocation", + ) as String?; } // auto backup frequency type @@ -695,12 +676,10 @@ class Prefs extends ChangeNotifier { } Future _getBackupFrequencyType() async { - String? rate = - await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "backupFrequencyType", - ) - as String?; + String? rate = await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "backupFrequencyType", + ) as String?; rate ??= "10Min"; switch (rate) { case "10Min": @@ -734,10 +713,9 @@ class Prefs extends ChangeNotifier { Future _getLastAutoBackup() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "lastAutoBackup", - ) - as DateTime?; + boxName: DB.boxNamePrefs, + key: "lastAutoBackup", + ) as DateTime?; } // auto backup @@ -763,10 +741,9 @@ class Prefs extends ChangeNotifier { Future _getHideBlockExplorerWarning() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "hideBlockExplorerWarning", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "hideBlockExplorerWarning", + ) as bool? ?? false; } @@ -793,10 +770,9 @@ class Prefs extends ChangeNotifier { Future _getGotoWalletOnStartup() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "gotoWalletOnStartup", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "gotoWalletOnStartup", + ) as bool? ?? false; } @@ -820,10 +796,9 @@ class Prefs extends ChangeNotifier { Future _getStartupWalletId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "startupWalletId", - ) - as String?; + boxName: DB.boxNamePrefs, + key: "startupWalletId", + ) as String?; } // incognito mode off by default @@ -849,10 +824,9 @@ class Prefs extends ChangeNotifier { Future _getHasExternalCalls() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "externalCalls", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "externalCalls", + ) as bool? ?? true; } @@ -871,9 +845,10 @@ class Prefs extends ChangeNotifier { String? get userID => _userId; Future _getUserId() async { - String? userID = - await DB.instance.get(boxName: DB.boxNamePrefs, key: "userID") - as String?; + String? userID = await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "userID", + ) as String?; if (userID == null) { userID = const Uuid().v4(); await saveUserID(userID); @@ -895,12 +870,10 @@ class Prefs extends ChangeNotifier { int? get signupEpoch => _signupEpoch; Future _getSignupEpoch() async { - int? signupEpoch = - await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "signupEpoch", - ) - as int?; + int? signupEpoch = await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "signupEpoch", + ) as int?; if (signupEpoch == null) { signupEpoch = DateTime.now().millisecondsSinceEpoch ~/ @@ -940,10 +913,9 @@ class Prefs extends ChangeNotifier { Future _getEnableCoinControl() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "enableCoinControl", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "enableCoinControl", + ) as bool? ?? false; } @@ -967,10 +939,9 @@ class Prefs extends ChangeNotifier { Future _getEnableSystemBrightness() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "enableSystemBrightness", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "enableSystemBrightness", + ) as bool? ?? false; } @@ -994,10 +965,9 @@ class Prefs extends ChangeNotifier { Future _getThemeId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "themeId", - ) - as String? ?? + boxName: DB.boxNamePrefs, + key: "themeId", + ) as String? ?? "light"; } @@ -1021,10 +991,9 @@ class Prefs extends ChangeNotifier { Future _getSystemBrightnessLightThemeId() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "systemBrightnessLightThemeId", - ) - as String? ?? + boxName: DB.boxNamePrefs, + key: "systemBrightnessLightThemeId", + ) as String? ?? "light"; } @@ -1048,10 +1017,9 @@ class Prefs extends ChangeNotifier { Future _getSystemBrightnessDarkTheme() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "systemBrightnessDarkThemeId", - ) - as String? ?? + boxName: DB.boxNamePrefs, + key: "systemBrightnessDarkThemeId", + ) as String? ?? "dark"; } @@ -1081,10 +1049,9 @@ class Prefs extends ChangeNotifier { for (final coin in AppConfig.coins) { final unitIndex = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "amountUnitFor${coin.identifier}", - ) - as int? ?? + boxName: DB.boxNamePrefs, + key: "amountUnitFor${coin.identifier}", + ) as int? ?? 0; // 0 is "normal" _amountUnits[coin] = AmountUnit.values[unitIndex]; } @@ -1116,10 +1083,9 @@ class Prefs extends ChangeNotifier { for (final coin in AppConfig.coins) { final decimals = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "maxDecimalsFor${coin.identifier}", - ) - as int? ?? + boxName: DB.boxNamePrefs, + key: "maxDecimalsFor${coin.identifier}", + ) as int? ?? (coin.fractionDigits > 18 ? 18 : coin.fractionDigits); // use some sane max rather than up to 30 that nano uses _amountDecimals[coin.identifier] = decimals; @@ -1152,10 +1118,9 @@ class Prefs extends ChangeNotifier { Future _getUseTor() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "useTor", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "useTor", + ) as bool? ?? false; } @@ -1184,30 +1149,25 @@ class Prefs extends ChangeNotifier { } Future> _getFusionServerInfo() async { - final map = - await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "fusionServerInfoMap", - ) - as Map?; + final map = await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "fusionServerInfoMap", + ) as Map?; if (map == null) { return _fusionServerInfo; } - final actualMap = Map.from( - map, - ).map((key, value) => MapEntry(key, FusionInfo.fromJsonString(value))); + final actualMap = Map.from(map) + .map((key, value) => MapEntry(key, FusionInfo.fromJsonString(value))); // legacy bch check if (actualMap["bitcoincash"] == null || actualMap["bitcoincashTestnet"] == null) { - final saved = - await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "fusionServerInfo", - ) - as String?; + final saved = await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "fusionServerInfo", + ) as String?; if (saved != null) { final bchInfo = FusionInfo.fromJsonString(saved); @@ -1248,10 +1208,9 @@ class Prefs extends ChangeNotifier { Future _getAutoPin() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoPin", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "autoPin", + ) as bool? ?? false; } @@ -1275,10 +1234,9 @@ class Prefs extends ChangeNotifier { Future _getEnableExchange() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "showExchange", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "showExchange", + ) as bool? ?? true; } @@ -1299,10 +1257,9 @@ class Prefs extends ChangeNotifier { Future _getAdvancedFiroFeatures() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "advancedFiroFeatures", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "advancedFiroFeatures", + ) as bool? ?? false; } @@ -1323,10 +1280,9 @@ class Prefs extends ChangeNotifier { Future _getLogsPath() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "logsPath", - ) - as String?; + boxName: DB.boxNamePrefs, + key: "logsPath", + ) as String?; } // log level pref @@ -1345,12 +1301,10 @@ class Prefs extends ChangeNotifier { } Future _getLogLevel() async { - final value = - await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "logLevel", - ) - as int?; + final value = await DB.instance.get( + boxName: DB.boxNamePrefs, + key: "logLevel", + ) as int?; try { return Level.values.firstWhere((e) => e.value == value); @@ -1384,10 +1338,9 @@ class Prefs extends ChangeNotifier { Future _getAutoLockInfo() async { final map = await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "autoLockInfo", - ) - as Map? ?? + boxName: DB.boxNamePrefs, + key: "autoLockInfo", + ) as Map? ?? {"enabled": false, "minutes": 10}; return (enabled: map["enabled"] as bool, minutes: map["minutes"] as int); @@ -1410,10 +1363,9 @@ class Prefs extends ChangeNotifier { Future _getPrivacyScreen() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "privacyScreen", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "privacyScreen", + ) as bool? ?? false; } @@ -1434,10 +1386,9 @@ class Prefs extends ChangeNotifier { Future _getDisableScreenShots() async { return await DB.instance.get( - boxName: DB.boxNamePrefs, - key: "disableScreenShots", - ) - as bool? ?? + boxName: DB.boxNamePrefs, + key: "disableScreenShots", + ) as bool? ?? false; } } From 1fa2797f91112233ac43661bd1c87ea4fe226d74 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 24 Aug 2026 18:49:48 -0500 Subject: [PATCH 2/3] feat: add event-driven auto backup triggers after wallet changes Fire a debounced auto-backup when a transaction note is saved or an address book contact is created, edited, or deleted, under a new "After editing a note or contact" backup frequency. AutoSWBService becomes provider-owned and takes its enabled/frequency gate as a predicate that is re-evaluated when the debounce fires, so turning auto backup off inside the debounce window cancels the pending backup and an SWB restore cannot trigger a backup storm. Changes arriving during an active backup queue a single follow-up instead of being dropped, and timers and notifications are inert after disposal. Rewriting the "default" self contact with identical data, which both address book views do on every open, no longer counts as an edit. closes #306 --- lib/main.dart | 3 + .../edit_auto_backup_view.dart | 4 + .../backup_frequency_type_select_sheet.dart | 2 + .../transaction_views/edit_note_view.dart | 8 + .../create_auto_backup.dart | 4 + .../global/address_book_service_provider.dart | 7 +- .../global/auto_swb_service_provider.dart | 17 +- lib/services/address_book_service.dart | 20 ++ lib/services/auto_swb_service.dart | 176 +++++++++++------- .../enums/backup_frequency_type.dart | 7 +- lib/utilities/format.dart | 2 + lib/utilities/prefs.dart | 9 + test/services/auto_swb_service_test.dart | 163 ++++++++++++++++ 13 files changed, 345 insertions(+), 77 deletions(-) create mode 100644 test/services/auto_swb_service_test.dart diff --git a/lib/main.dart b/lib/main.dart index 04ed3daa6d..ff31239834 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -478,6 +478,9 @@ class _MaterialAppWithThemeState extends ConsumerState case BackupFrequencyType.afterClosingAWallet: // ignore this case here break; + case BackupFrequencyType.afterChanges: + ref.read(autoSWBServiceProvider); + break; } } diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index f21a7bdd56..5fdece0fed 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -73,6 +73,7 @@ class _EditAutoBackupViewState extends ConsumerState { BackupFrequencyType.everyTenMinutes, BackupFrequencyType.everyAppStart, BackupFrequencyType.afterClosingAWallet, + BackupFrequencyType.afterChanges, ]; String passwordFeedback = @@ -571,6 +572,9 @@ class _EditAutoBackupViewState extends ConsumerState { case BackupFrequencyType.afterClosingAWallet: message = "After closing a cryptocurrency wallet"; break; + case BackupFrequencyType.afterChanges: + message = "After editing a note or contact"; + break; } return DropdownMenuItem( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart index 8d578a5eb3..ab75955516 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/sub_views/backup_frequency_type_select_sheet.dart @@ -28,6 +28,8 @@ class BackupFrequencyTypeSelectSheet extends ConsumerWidget { return "Every app start"; case BackupFrequencyType.afterClosingAWallet: return "After closing a cryptocurrency wallet"; + case BackupFrequencyType.afterChanges: + return "After editing a note or contact"; } } diff --git a/lib/pages/wallet_view/transaction_views/edit_note_view.dart b/lib/pages/wallet_view/transaction_views/edit_note_view.dart index 480ddc66eb..72e7f869e7 100644 --- a/lib/pages/wallet_view/transaction_views/edit_note_view.dart +++ b/lib/pages/wallet_view/transaction_views/edit_note_view.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/isar/models/transaction_note.dart'; +import '../../../providers/global/auto_swb_service_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/constants.dart'; @@ -195,6 +196,10 @@ class _EditNoteViewState extends ConsumerState { ), ); + ref + .read(autoSWBServiceProvider) + .requestBackupAfterChange(); + if (context.mounted) { Navigator.of(context).pop(); } @@ -214,6 +219,9 @@ class _EditNoteViewState extends ConsumerState { value: _noteController.text, ), ); + + ref.read(autoSWBServiceProvider).requestBackupAfterChange(); + if (context.mounted) { Navigator.of(context).pop(); } diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index 070ca48f16..49c04356d2 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -86,6 +86,7 @@ class _CreateAutoBackup extends ConsumerState { BackupFrequencyType.everyTenMinutes, BackupFrequencyType.everyAppStart, BackupFrequencyType.afterClosingAWallet, + BackupFrequencyType.afterChanges, ]; Future _enableAutoBackup() async { @@ -616,6 +617,9 @@ class _CreateAutoBackup extends ConsumerState { case BackupFrequencyType.afterClosingAWallet: message = "After closing a cryptocurrency wallet"; break; + case BackupFrequencyType.afterChanges: + message = "After editing a note or contact"; + break; } return DropdownMenuItem( diff --git a/lib/providers/global/address_book_service_provider.dart b/lib/providers/global/address_book_service_provider.dart index c948360601..442464ddf3 100644 --- a/lib/providers/global/address_book_service_provider.dart +++ b/lib/providers/global/address_book_service_provider.dart @@ -11,7 +11,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../services/address_book_service.dart'; +import 'auto_swb_service_provider.dart'; final addressBookServiceProvider = ChangeNotifierProvider( - (ref) => AddressBookService(), + (ref) => AddressBookService( + requestAutoBackup: ref + .read(autoSWBServiceProvider) + .requestBackupAfterChange, + ), ); diff --git a/lib/providers/global/auto_swb_service_provider.dart b/lib/providers/global/auto_swb_service_provider.dart index 5310ee3ce0..0b3d1f6699 100644 --- a/lib/providers/global/auto_swb_service_provider.dart +++ b/lib/providers/global/auto_swb_service_provider.dart @@ -10,10 +10,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'secure_store_provider.dart'; import '../../services/auto_swb_service.dart'; +import '../../utilities/enums/backup_frequency_type.dart'; +import 'prefs_provider.dart'; +import 'secure_store_provider.dart'; -final autoSWBServiceProvider = ChangeNotifierProvider( - (ref) => - AutoSWBService(secureStorageInterface: ref.read(secureStoreProvider)), -); +final autoSWBServiceProvider = ChangeNotifierProvider((ref) { + final prefs = ref.read(prefsChangeNotifierProvider); + return AutoSWBService( + secureStorageInterface: ref.read(secureStoreProvider), + shouldBackupAfterChange: () => + prefs.isAutoBackupEnabled && + prefs.backupFrequencyType == BackupFrequencyType.afterChanges, + ); +}); diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 17381a6bec..0ccf3ef25f 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -8,6 +8,8 @@ * */ +import 'dart:convert'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -15,6 +17,10 @@ import '../db/isar/main_db.dart'; import '../models/isar/models/contact_entry.dart'; class AddressBookService extends ChangeNotifier { + AddressBookService({this.requestAutoBackup}); + + final void Function()? requestAutoBackup; + ContactEntry getContactById(String id) { final ContactEntry? contactEntry = MainDB.instance.getContactEntry(id: id); if (contactEntry == null) { @@ -67,15 +73,28 @@ class AddressBookService extends ChangeNotifier { } else { await MainDB.instance.putContactEntry(contactEntry: contact); notifyListeners(); + requestAutoBackup?.call(); return true; } } /// Edit contact Future editContact(ContactEntry editedContact) async { + // The address book views rewrite the "default" self contact on every open. + // An identical write is not a user edit, so it must not notify listeners or + // trigger an auto backup. + final existing = MainDB.instance.getContactEntry( + id: editedContact.customId, + ); + if (existing != null && + jsonEncode(existing.toMap()) == jsonEncode(editedContact.toMap())) { + return true; + } + // over write the contact with edited version await MainDB.instance.putContactEntry(contactEntry: editedContact); notifyListeners(); + requestAutoBackup?.call(); return true; } @@ -83,5 +102,6 @@ class AddressBookService extends ChangeNotifier { Future removeContact(String id) async { await MainDB.instance.deleteContactEntry(id: id); notifyListeners(); + requestAutoBackup?.call(); } } diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index 419e7a0804..2db12ebdd1 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -25,6 +25,9 @@ enum AutoSWBStatus { idle, backingUp, error } class AutoSWBService extends ChangeNotifier { Timer? _timer; + Timer? _debounceTimer; + bool _backupPending = false; + bool _isDisposed = false; AutoSWBStatus _status = AutoSWBStatus.idle; AutoSWBStatus get status => _status; @@ -33,95 +36,124 @@ class AutoSWBService extends ChangeNotifier { bool get isActivePeriodicTimer => _isActiveTimer; final SecureStorageInterface secureStorageInterface; + final bool Function() shouldBackupAfterChange; + @visibleForTesting + final Future Function()? backupRunner; + final Duration debounceDuration; + + AutoSWBService({ + required this.secureStorageInterface, + required this.shouldBackupAfterChange, + this.backupRunner, + this.debounceDuration = const Duration(seconds: 5), + }); + + void requestBackupAfterChange() { + if (_isDisposed || !shouldBackupAfterChange()) return; + + Logging.instance.d("AutoSWBService.requestBackupAfterChange() triggered"); + // Re-checked when the timer fires: a backup writes wallet seed material, + // and the user can turn auto backup off inside the debounce window. + requestBackup(onlyIf: shouldBackupAfterChange); + } - AutoSWBService({required this.secureStorageInterface}); + void requestBackup({Duration? debounceDuration, bool Function()? onlyIf}) { + if (_isDisposed) return; + _debounceTimer?.cancel(); + _debounceTimer = Timer(debounceDuration ?? this.debounceDuration, () { + _debounceTimer = null; + if (_isDisposed || (onlyIf != null && !onlyIf())) { + Logging.instance.d( + "AutoSWBService.requestBackup() debounce fired but the trigger " + "condition no longer holds; skipping backup", + ); + return; + } + Logging.instance.d( + "AutoSWBService.requestBackup() debounce fired, running doBackup()", + ); + unawaited(doBackup()); + }); + } /// Attempt a backup. Future doBackup() async { + if (_isDisposed) return; if (_status == AutoSWBStatus.backingUp) { - Logging.instance.w( - "AutoSWBService attempted to run doBackup() while a backup is in progress!", - ); + _backupPending = true; return; } - Logging.instance.d("AutoSWBService.doBackup() started..."); - - // set running backup status and notify listeners - _status = AutoSWBStatus.backingUp; - notifyListeners(); - - try { - if (!Prefs.instance.isInitialized) { - await Prefs.instance.init(); - } - final autoBackupDirectoryPath = Prefs.instance.autoBackupLocation; - if (autoBackupDirectoryPath == null) { - Logging.instance.e( - "AutoSWBService attempted to run doBackup() when no auto backup directory was set!", - ); - // set error backup status and notify listeners - _status = AutoSWBStatus.error; - notifyListeners(); - return; - } - - final json = await SWB.createStackWalletJSON( - secureStorage: secureStorageInterface, - ); - final jsonString = jsonEncode(json); - - final adkString = await secureStorageInterface.read( - key: "auto_adk_string", - ); - - final adkVersionString = await secureStorageInterface.read( - key: "auto_adk_version_string", - ); - final int adkVersion = int.parse(adkVersionString!); - - final DateTime now = DateTime.now(); - final String fileToSave = createAutoBackupFilename( - autoBackupDirectoryPath, - now, - ); - - final content = await SWB.encryptStackWalletWithADK( - adkString!, - jsonString, - adkVersion, - ); - - await FS.writeStringToFile( - content, - autoBackupDirectoryPath, - fileToSave.split("/").last, - ); - - Prefs.instance.lastAutoBackup = now; + do { + _backupPending = false; + await _doBackupOnce(); + } while (_backupPending && !_isDisposed); + } - // delete all but the latest 3 auto backups - trimBackups(autoBackupDirectoryPath, 3); + Future _doBackupOnce() async { + Logging.instance.d("AutoSWBService.doBackup() started..."); + _setStatus(AutoSWBStatus.backingUp); + try { + await (backupRunner?.call() ?? _writeBackup()); Logging.instance.d("AutoSWBService.doBackup() succeeded"); } on Exception catch (e, s) { final String err = getErrorMessageFromSWBException(e); Logging.instance.e("$err\n$s", error: e, stackTrace: s); - // set error backup status and notify listeners - _status = AutoSWBStatus.error; - notifyListeners(); + _setStatus(AutoSWBStatus.error); return; } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); - // set error backup status and notify listeners - _status = AutoSWBStatus.error; - notifyListeners(); + _setStatus(AutoSWBStatus.error); return; } - // set done/idle backup status and notify listeners - _status = AutoSWBStatus.idle; - notifyListeners(); + _setStatus(AutoSWBStatus.idle); + } + + Future _writeBackup() async { + if (!Prefs.instance.isInitialized) { + await Prefs.instance.init(); + } + + final autoBackupDirectoryPath = Prefs.instance.autoBackupLocation; + if (autoBackupDirectoryPath == null) { + throw StateError("No auto backup directory is set"); + } + + final json = await SWB.createStackWalletJSON( + secureStorage: secureStorageInterface, + ); + final jsonString = jsonEncode(json); + + final adkString = await secureStorageInterface.read(key: "auto_adk_string"); + final adkVersionString = await secureStorageInterface.read( + key: "auto_adk_version_string", + ); + final adkVersion = int.parse(adkVersionString!); + final now = DateTime.now(); + final fileToSave = createAutoBackupFilename(autoBackupDirectoryPath, now); + final content = await SWB.encryptStackWalletWithADK( + adkString!, + jsonString, + adkVersion, + ); + + await FS.writeStringToFile( + content, + autoBackupDirectoryPath, + fileToSave.split("/").last, + ); + + Prefs.instance.lastAutoBackup = now; + trimBackups(autoBackupDirectoryPath, 3); + } + + void _setStatus(AutoSWBStatus status) { + _status = status; + if (!_isDisposed) { + notifyListeners(); + } } /// Trim the number of auto backup files based on age @@ -165,7 +197,7 @@ class AutoSWBService extends ChangeNotifier { (a, b) => b.item1.millisecondsSinceEpoch - a.item1.millisecondsSinceEpoch, ); - // delete any older backups if there are more than the number we want to keep + // Delete backups beyond the retention limit. while (files.length > numberToKeep) { final fileToDelete = files.removeLast().item2; fileToDelete.deleteSync(); @@ -199,6 +231,10 @@ class AutoSWBService extends ChangeNotifier { @override void dispose() { + _isDisposed = true; + _backupPending = false; + _debounceTimer?.cancel(); + _debounceTimer = null; stopPeriodicBackupTimer(shouldNotifyListeners: false); super.dispose(); } diff --git a/lib/utilities/enums/backup_frequency_type.dart b/lib/utilities/enums/backup_frequency_type.dart index 6a0992ff36..2c85ebde7d 100644 --- a/lib/utilities/enums/backup_frequency_type.dart +++ b/lib/utilities/enums/backup_frequency_type.dart @@ -8,4 +8,9 @@ * */ -enum BackupFrequencyType { everyTenMinutes, everyAppStart, afterClosingAWallet } +enum BackupFrequencyType { + everyTenMinutes, + everyAppStart, + afterClosingAWallet, + afterChanges, +} diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index 79ec8aecb6..808f8692ae 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -142,6 +142,8 @@ abstract class Format { return "Every app start"; case BackupFrequencyType.afterClosingAWallet: return "After closing a cryptocurrency wallet"; + case BackupFrequencyType.afterChanges: + return "After editing a note or contact"; } } } diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 36677bd6d6..59c89fdc6a 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -669,6 +669,13 @@ class Prefs extends ChangeNotifier { value: "onWalletClose", ); break; + case BackupFrequencyType.afterChanges: + DB.instance.put( + boxName: DB.boxNamePrefs, + key: "backupFrequencyType", + value: "afterChanges", + ); + break; } _backupFrequencyType = backupFrequencyType; notifyListeners(); @@ -688,6 +695,8 @@ class Prefs extends ChangeNotifier { return BackupFrequencyType.everyAppStart; case "onWalletClose": return BackupFrequencyType.afterClosingAWallet; + case "afterChanges": + return BackupFrequencyType.afterChanges; default: throw Exception("Invalid Backup Frequency type found in prefs!"); } diff --git a/test/services/auto_swb_service_test.dart b/test/services/auto_swb_service_test.dart new file mode 100644 index 0000000000..522b94b27f --- /dev/null +++ b/test/services/auto_swb_service_test.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/services/auto_swb_service.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; + +void main() { + late AutoSWBService service; + late Future Function() backupRunner; + var shouldBackup = true; + var isDisposed = false; + + setUp(() { + shouldBackup = true; + isDisposed = false; + backupRunner = () async {}; + service = AutoSWBService( + secureStorageInterface: FakeSecureStorage(), + shouldBackupAfterChange: () => shouldBackup, + backupRunner: () => backupRunner(), + debounceDuration: Duration.zero, + ); + }); + + tearDown(() { + if (!isDisposed) { + service.dispose(); + } + }); + + test('only schedules change backups when enabled', () async { + var backups = 0; + backupRunner = () async { + backups++; + }; + + shouldBackup = false; + service.requestBackupAfterChange(); + await _flushTimers(); + expect(backups, 0); + + shouldBackup = true; + service.requestBackupAfterChange(); + await _flushTimers(); + expect(backups, 1); + }); + + test( + 'does not back up if the gate closes inside the debounce window', + () async { + var backups = 0; + backupRunner = () async { + backups++; + }; + + service.requestBackupAfterChange(); + // User turns auto backup off (or switches frequency) before the timer fires. + shouldBackup = false; + await _flushTimers(); + + expect(backups, 0); + }, + ); + + test('coalesces a burst into one backup', () async { + var backups = 0; + backupRunner = () async { + backups++; + }; + + service.requestBackupAfterChange(); + service.requestBackupAfterChange(); + service.requestBackupAfterChange(); + await _flushTimers(); + + expect(backups, 1); + expect(service.status, AutoSWBStatus.idle); + }); + + test('runs one follow-up for changes during an active backup', () async { + final firstBackup = Completer(); + var backups = 0; + backupRunner = () async { + backups++; + if (backups == 1) { + await firstBackup.future; + } + }; + + final runningBackup = service.doBackup(); + await _flushTimers(); + expect(service.status, AutoSWBStatus.backingUp); + + service.requestBackupAfterChange(); + service.requestBackupAfterChange(); + await _flushTimers(); + expect(backups, 1); + + firstBackup.complete(); + await runningBackup; + + expect(backups, 2); + expect(service.status, AutoSWBStatus.idle); + }); + + test('recovers after a backup failure', () async { + var backups = 0; + backupRunner = () async { + backups++; + if (backups == 1) { + throw StateError('failed'); + } + }; + + await service.doBackup(); + expect(service.status, AutoSWBStatus.error); + + await service.doBackup(); + expect(backups, 2); + expect(service.status, AutoSWBStatus.idle); + }); + + test('cancels a debounced request on dispose', () async { + var backups = 0; + backupRunner = () async { + backups++; + }; + service.requestBackup(debounceDuration: const Duration(milliseconds: 20)); + + service.dispose(); + isDisposed = true; + await Future.delayed(const Duration(milliseconds: 30)); + + expect(backups, 0); + }); + + test('finishes an active backup safely after dispose', () async { + final backup = Completer(); + var backups = 0; + backupRunner = () async { + backups++; + await backup.future; + }; + + final runningBackup = service.doBackup(); + await _flushTimers(); + service.dispose(); + isDisposed = true; + backup.complete(); + + await expectLater(runningBackup, completes); + service.requestBackupAfterChange(); + await _flushTimers(); + + expect(backups, 1); + expect(service.status, AutoSWBStatus.idle); + }); +} + +Future _flushTimers() async { + await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); +} From f3bfdef6f91676da7746f273a342358742b769db Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 7 Oct 2026 13:10:36 -0500 Subject: [PATCH 3/3] fix(backup): recheck queued change backup conditions --- lib/services/auto_swb_service.dart | 17 +++++++++-------- test/services/auto_swb_service_test.dart | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart index 2db12ebdd1..03e63a2959 100644 --- a/lib/services/auto_swb_service.dart +++ b/lib/services/auto_swb_service.dart @@ -26,7 +26,7 @@ enum AutoSWBStatus { idle, backingUp, error } class AutoSWBService extends ChangeNotifier { Timer? _timer; Timer? _debounceTimer; - bool _backupPending = false; + final _pendingBackups = {}; bool _isDisposed = false; AutoSWBStatus _status = AutoSWBStatus.idle; @@ -72,22 +72,23 @@ class AutoSWBService extends ChangeNotifier { Logging.instance.d( "AutoSWBService.requestBackup() debounce fired, running doBackup()", ); - unawaited(doBackup()); + unawaited(doBackup(onlyIf: onlyIf)); }); } /// Attempt a backup. - Future doBackup() async { - if (_isDisposed) return; + Future doBackup({bool Function()? onlyIf}) async { + if (_isDisposed || !(onlyIf?.call() ?? true)) return; if (_status == AutoSWBStatus.backingUp) { - _backupPending = true; + _pendingBackups.add(onlyIf); return; } do { - _backupPending = false; + _pendingBackups.clear(); await _doBackupOnce(); - } while (_backupPending && !_isDisposed); + } while (!_isDisposed && + _pendingBackups.any((condition) => condition?.call() ?? true)); } Future _doBackupOnce() async { @@ -232,7 +233,7 @@ class AutoSWBService extends ChangeNotifier { @override void dispose() { _isDisposed = true; - _backupPending = false; + _pendingBackups.clear(); _debounceTimer?.cancel(); _debounceTimer = null; stopPeriodicBackupTimer(shouldNotifyListeners: false); diff --git a/test/services/auto_swb_service_test.dart b/test/services/auto_swb_service_test.dart index 522b94b27f..5660a4aeb8 100644 --- a/test/services/auto_swb_service_test.dart +++ b/test/services/auto_swb_service_test.dart @@ -103,6 +103,23 @@ void main() { expect(service.status, AutoSWBStatus.idle); }); + test('does not run a queued change backup after disabling backups', () async { + final firstBackup = Completer(); + var backups = 0; + backupRunner = () async { + backups++; + if (backups == 1) await firstBackup.future; + }; + final runningBackup = service.doBackup(); + await _flushTimers(); + service.requestBackupAfterChange(); + await _flushTimers(); + shouldBackup = false; + firstBackup.complete(); + await runningBackup; + expect(backups, 1); + }); + test('recovers after a backup failure', () async { var backups = 0; backupRunner = () async {