feat: persist whether a wallet still needs its recovery phrase backed up
What changed, and why it matters
This commit adds a persistent reminder for users to back up their wallet recovery phrase. It stores a new flag in the app's database, shows a reminder on the dashboard when the wallet has funds, and clears the reminder once the user completes a seed verification flow. It also adds the seed page to the list of screens that always require authentication before opening. There is no direct security vulnerability in the change; it is a user-experience and safety improvement.
No security patch is needed. Reviewers may want to confirm that the new `Routes.seed` authentication requirement does not break legitimate user flows and that the migration safely handles existing wallets.
Security signals we found
New persistent flag tracks whether recovery phrase backup is still pending
Seed display route added to always-authenticate route list
Reminder visibility gated on wallet balance being non-zero
Reminder cleared after user completes seed verification
Database migration adds column with default FALSE
Evidence from the diff
The change introduces a showSeedBackupReminder boolean column in the WalletInfo SQLite table with a migration from schema version 10 to 11. The flag is read into the dashboard view model, where a computed shouldShowSeedBackupReminder is true only when the flag is set and the wallet has a non-zero balance. The reminder can be dismissed via dismissSeedBackupReminder, and completing seed verification in WalletSeedViewModel calls markSeedVerified to clear the flag. Additionally, Routes.seed is added to _alwaysAuthenticateRoutes in AuthService, so the seed screen requires authentication.
Changed components
cw_core/lib/db/sqlite.dartcw_core/lib/wallet_info.dartcw_core/lib/wallet_info_legacy.dartlib/core/auth_service.dartlib/view_model/dashboard/dashboard_view_model.dartlib/view_model/wallet_seed_view_model.dartInspect captured patch +75 / −6
diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart
index 0deffab..b6e0dd2 100644
--- a/cw_core/lib/db/sqlite.dart
+++ b/cw_core/lib/db/sqlite.dart
@@ -63,7 +63,7 @@ Future<void> _initDb({String? pathOverride}) async {
}
}
await db?.close();
- db = await openDatabase(dbFile.path, version: 10,
+ db = await openDatabase(dbFile.path, version: 11,
onUpgrade: (Database db, int oldVersion, int newVersion) async {
printV("migrating: $oldVersion, $newVersion");
if (oldVersion <= 1) {
@@ -152,6 +152,14 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
await _createSplTokenTable(db);
await _createTronTokenTable(db);
}
+ if (oldVersion <= 10) {
+ await _addColumnIfNotExists(
+ db,
+ table: 'WalletInfo',
+ column: 'showSeedBackupReminder',
+ definition: 'BOOLEAN DEFAULT FALSE',
+ );
+ }
}, onCreate: (Database db, int version) async {
await db.execute('''
CREATE TABLE WalletInfo (
@@ -178,7 +186,8 @@ CREATE TABLE WalletInfo (
sortOrder INTEGER DEFAULT (0) NOT NULL,
receiveInfoboxDismissed BOOLEAN DEFAULT FALSE,
showCombinedBalance BOOLEAN DEFAULT TRUE,
- favoriteTokenAddress TEXT DEFAULT NULL
+ favoriteTokenAddress TEXT DEFAULT NULL,
+ showSeedBackupReminder BOOLEAN DEFAULT FALSE
);
''');
diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart
index 585ce3b..61b6278 100644
--- a/cw_core/lib/wallet_info.dart
+++ b/cw_core/lib/wallet_info.dart
@@ -349,7 +349,8 @@ class WalletInfo {
this.addressPageType,
this.receiveInfoboxDismissed,
this.showCombinedBalance,
- this.favoriteTokenAddress)
+ this.favoriteTokenAddress,
+ this.showSeedBackupReminder)
: _yatLastUsedAddressController = StreamController<String>.broadcast();
factory WalletInfo.external(
@@ -397,7 +398,8 @@ class WalletInfo {
null,
receiveInfoboxDismissed ?? false,
showCombinedBalance ?? true,
- favoriteTokenAddress);
+ favoriteTokenAddress,
+ false);
}
static String get tableName => 'walletInfo';
@@ -418,6 +420,7 @@ class WalletInfo {
bool receiveInfoboxDismissed;
bool showCombinedBalance;
String? favoriteTokenAddress;
+ bool showSeedBackupReminder;
Future<Map<String, String>> getAddresses() async {
final list = await WalletInfoAddressMap.selectList(internalId);
@@ -587,6 +590,7 @@ class WalletInfo {
"receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0,
"showCombinedBalance": showCombinedBalance ? 1 : 0,
"favoriteTokenAddress": favoriteTokenAddress,
+ "showSeedBackupReminder": showSeedBackupReminder ? 1 : 0,
"network": network,
};
@@ -616,7 +620,8 @@ class WalletInfo {
json['addressPageType'] as String? ?? null,
json['receiveInfoboxDismissed'] != 0,
json["showCombinedBalance"] != 0,
- json["favoriteTokenAddress"] as String? ?? null);
+ json["favoriteTokenAddress"] as String? ?? null,
+ json['showSeedBackupReminder'] == 1);
info.network = json['network'] as String?;
return info;
}
@@ -664,4 +669,14 @@ class WalletInfo {
restoreHeight = height;
await save();
}
+
+ Future<void> updateShowSeedBackupReminder(bool show) async {
+ showSeedBackupReminder = show;
+
+ try {
+ await save();
+ } catch (e) {
+ printV("Failed to save the seed backup reminder flag: $e");
+ }
+ }
}
diff --git a/cw_core/lib/wallet_info_legacy.dart b/cw_core/lib/wallet_info_legacy.dart
index 2dadbf2..6f9ff9c 100644
--- a/cw_core/lib/wallet_info_legacy.dart
+++ b/cw_core/lib/wallet_info_legacy.dart
@@ -274,7 +274,8 @@ class WalletInfo extends HiveObject {
addressPageType,
false,
true,
- null)
+ null,
+ false)
..network = network;
final wiId = await walletInfo.save();
for (final address in usedAddresses ?? <String>[]) {
diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart
index 4871929..1057b62 100644
--- a/lib/core/auth_service.dart
+++ b/lib/core/auth_service.dart
@@ -36,6 +36,7 @@ class AuthService with Store {
static const List<String> _alwaysAuthenticateRoutes = [
Routes.showKeys,
+ Routes.seed,
Routes.backup,
Routes.setupPin,
Routes.setup_2faPage,
diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart
index 994121a..4b83c80 100644
--- a/lib/view_model/dashboard/dashboard_view_model.dart
+++ b/lib/view_model/dashboard/dashboard_view_model.dart
@@ -107,6 +107,7 @@ abstract class DashboardViewModelBase with Store {
wallet = appStore.wallet! {
showDecredInfoCard = wallet.type == WalletType.decred &&
(sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true);
+ showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
name = wallet.name;
type = wallet.type;
@@ -207,6 +208,7 @@ abstract class DashboardViewModelBase with Store {
loadCardDesigns();
showDecredInfoCard = wallet?.type == WalletType.decred &&
sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) != false;
+ loadSeedBackupReminder();
tradeMonitor.stopTradeMonitoring();
tradeMonitor.monitorActiveTrades(wallet!.id);
@@ -892,6 +894,22 @@ abstract class DashboardViewModelBase with Store {
@observable
late bool showDecredInfoCard;
+ @observable
+ late bool showSeedBackupReminder;
+
+ @computed
+ bool get hasBalance => wallet.balance.values.any(
+ (balance) =>
+ !balance.available.isZero ||
+ !balance.unavailable.isZero ||
+ !(balance.secondAvailable?.isZero ?? true) ||
+ !(balance.secondUnavailable?.isZero ?? true) ||
+ !(balance.frozen?.isZero ?? true),
+ );
+
+ @computed
+ bool get shouldShowSeedBackupReminder => showSeedBackupReminder && hasBalance;
+
@computed
bool get showPayjoinCard =>
wallet.type == WalletType.bitcoin &&
@@ -1121,6 +1139,17 @@ abstract class DashboardViewModelBase with Store {
sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false);
}
+ @action
+ void loadSeedBackupReminder() {
+ showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
+ }
+
+ @action
+ Future<void> dismissSeedBackupReminder() async {
+ showSeedBackupReminder = false;
+ await wallet.walletInfo.updateShowSeedBackupReminder(false);
+ }
+
@action
void dismissPayjoin() {
settingsStore.showPayjoinCard = false;
diff --git a/lib/view_model/wallet_seed_view_model.dart b/lib/view_model/wallet_seed_view_model.dart
index 4d3ee97..9894dc4 100644
--- a/lib/view_model/wallet_seed_view_model.dart
+++ b/lib/view_model/wallet_seed_view_model.dart
@@ -1,7 +1,9 @@
+import 'dart:async';
import 'dart:math';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/wallet_info.dart';
import 'package:flutter/foundation.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@@ -16,11 +18,14 @@ abstract class WalletSeedViewModelBase with Store {
: name = wallet.name,
seed = wallet.seed!,
walletType = wallet.type,
+ _walletInfo = wallet.walletInfo,
currentOptions = ObservableList<String>(),
verificationIndices = ObservableList<int>() {
setupSeedVerification();
}
+ final WalletInfo _walletInfo;
+
@observable
String name;
@@ -133,6 +138,7 @@ abstract class WalletSeedViewModelBase with Store {
} else {
// All verification steps completed
isVerificationComplete = true;
+ unawaited(markSeedVerified());
}
return true;
@@ -141,4 +147,12 @@ abstract class WalletSeedViewModelBase with Store {
return false;
}
}
+
+ Future<void> markSeedVerified() async {
+ if (!_walletInfo.showSeedBackupReminder) {
+ return;
+ }
+
+ await _walletInfo.updateShowSeedBackupReminder(false);
+ }
}
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.