feat: remind users with a balance to back up their recovery phrase
What changed, and why it matters
This commit adds a friendly in-app reminder for users who have cryptocurrency in their wallet but have not yet backed up their recovery phrase. It shows a card on the home screen and offers a guided flow to view or dismiss the reminder. There is no security vulnerability here; it is a user-experience and safety feature.
No security action required. Treat as a normal feature review; verify the reminder visibility logic and dismissal persistence behave as intended.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces a new SeedBackupReminderCard on the home page, a SeedBackupReminderPage, and a DismissSeedVerificationPage. It wires the dashboard view model to decide when to show the reminder (shouldShowSeedBackupReminder) and to dismiss it (dismissSeedBackupReminder). The reminder routes to the existing authenticated seed display flow. Minor cleanups include fixing an import typo in a test robot, removing an obsolete comment, and standardizing quote styles on some imports.
Changed components
lib/new-ui/pages/home_page.dartlib/new-ui/pages/seed/seed_backup_reminder_page.dartlib/new-ui/pages/seed/dismiss_seed_verification_page.dartlib/new-ui/widgets/coins_page/seed_backup_reminder_card.dartlib/src/screens/seed/seed_verification/seed_verification_success_view.dartlib/router.dartlib/new-ui/pages/seed/open_wallet_after_seed_flow.dartintegration_test/robots/pre_seed_page_robot.dartInspect captured patch +249 / −12
diff --git a/integration_test/robots/pre_seed_page_robot.dart b/integration_test/robots/pre_seed_page_robot.dart
index c3eeb5f..0e0eab4 100644
--- a/integration_test/robots/pre_seed_page_robot.dart
+++ b/integration_test/robots/pre_seed_page_robot.dart
@@ -1,4 +1,4 @@
-import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dar"';
+import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart";
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
diff --git a/lib/new-ui/pages/home_page.dart b/lib/new-ui/pages/home_page.dart
index f624409..d34151d 100644
--- a/lib/new-ui/pages/home_page.dart
+++ b/lib/new-ui/pages/home_page.dart
@@ -1,15 +1,18 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
+import "package:cake_wallet/main.dart";
import 'package:cake_wallet/new-ui/modal_navigator.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
+import "package:cake_wallet/new-ui/pages/seed/seed_backup_reminder_page.dart";
import 'package:cake_wallet/new-ui/pages/settings_page.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
+import "package:cake_wallet/new-ui/widgets/coins_page/seed_backup_reminder_card.dart";
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
@@ -18,9 +21,6 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
-import "package:cw_core/amount/money.dart";
-import "package:cw_core/crypto_currency.dart";
-import "package:cw_core/transaction_direction.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -38,7 +38,7 @@ class NewHomePage extends StatefulWidget {
State<NewHomePage> createState() => _NewHomePageState();
}
-class _NewHomePageState extends State<NewHomePage> {
+class _NewHomePageState extends State<NewHomePage> with RouteAware {
MoneroAccountListViewModel? accountListViewModel;
bool _lightningMode = false;
@@ -53,9 +53,9 @@ class _NewHomePageState extends State<NewHomePage> {
});
});
- reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
+ reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) {
- if(!context.mounted) {
+ if (!context.mounted) {
return;
}
widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true;
@@ -72,6 +72,21 @@ class _NewHomePageState extends State<NewHomePage> {
});
}
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
+ }
+
+ @override
+ void dispose() {
+ routeObserver.unsubscribe(this);
+ super.dispose();
+ }
+
+ @override
+ void didPopNext() => widget.dashboardViewModel.loadSeedBackupReminder();
+
void _setAccountViewModel() {
accountListViewModel = widget.dashboardViewModel.balanceViewModel.hasAccounts
? getIt.get<MoneroAccountListViewModel>()
@@ -191,6 +206,10 @@ class _NewHomePageState extends State<NewHomePage> {
MwebAd(
dashboardViewModel: widget.dashboardViewModel,
),
+ SeedBackupReminderCard(
+ dashboardViewModel: widget.dashboardViewModel,
+ onTap: openSeedBackupReminder,
+ ),
],
);
},
@@ -231,6 +250,17 @@ class _NewHomePageState extends State<NewHomePage> {
);
}
+ void openSeedBackupReminder() {
+ Navigator.of(context).push(
+ CupertinoPageRoute<void>(
+ builder: (_) => SeedBackupReminderPage(
+ dashboardViewModel: widget.dashboardViewModel,
+ authService: getIt.get<AuthService>(),
+ ),
+ ),
+ );
+ }
+
void openAccountCustomizer() async {
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
diff --git a/lib/new-ui/pages/seed/dismiss_seed_verification_page.dart b/lib/new-ui/pages/seed/dismiss_seed_verification_page.dart
new file mode 100644
index 0000000..765168c
--- /dev/null
+++ b/lib/new-ui/pages/seed/dismiss_seed_verification_page.dart
@@ -0,0 +1,57 @@
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/widgets/seed/seed_page_header.dart";
+import "package:cake_wallet/new-ui/widgets/seed/seed_page_scaffold.dart";
+import "package:cake_wallet/new-ui/widgets/stacked_buttons.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:flutter/material.dart";
+
+class DismissSeedVerificationPage extends StatelessWidget {
+ const DismissSeedVerificationPage({required this.onConfirm, super.key});
+
+ final VoidCallback onConfirm;
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = Theme.of(context).colorScheme;
+
+ return SeedPageScaffold(
+ content: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 295),
+ child: SeedPageHeader(
+ image: const CakeImageWidget(
+ imageUrl: "assets/new-ui/warning_triangle.svg",
+ width: 125,
+ height: 111.72,
+ ),
+ imageSpacing: 48,
+ titleSpacing: 24,
+ title: S.of(context).dismiss_verification_title,
+ description: Text.rich(
+ TextSpan(
+ children: [
+ TextSpan(text: "${S.of(context).dismiss_verification_description}\n"),
+ TextSpan(
+ text: S.of(context).seed_display_path,
+ style: TextStyle(color: colors.onSurface),
+ ),
+ ],
+ ),
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: colors.onSurfaceVariant,
+ letterSpacing: -0.07,
+ ),
+ ),
+ ),
+ ),
+ footer: StackedButtons(
+ primaryKey: const ValueKey("dismiss_seed_verification_page_confirm_button_key"),
+ primaryText: S.of(context).dismiss_verification_confirm,
+ onPrimary: onConfirm,
+ secondaryKey: const ValueKey("dismiss_seed_verification_page_back_button_key"),
+ secondaryText: S.of(context).seed_alert_back,
+ onSecondary: () => Navigator.of(context).pop(),
+ ),
+ );
+ }
+}
diff --git a/lib/new-ui/pages/seed/open_wallet_after_seed_flow.dart b/lib/new-ui/pages/seed/open_wallet_after_seed_flow.dart
index 22d5511..edc102a 100644
--- a/lib/new-ui/pages/seed/open_wallet_after_seed_flow.dart
+++ b/lib/new-ui/pages/seed/open_wallet_after_seed_flow.dart
@@ -2,7 +2,6 @@ import "package:cake_wallet/routes.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/widgets.dart";
-// Bitcoin wallets set up lightning username right after the seed step.
void openWalletAfterSeedFlow(BuildContext context, WalletType walletType) {
if (walletType == WalletType.bitcoin) {
Navigator.of(context).pushNamed(Routes.lightningUsernamePage, arguments: true);
diff --git a/lib/new-ui/pages/seed/seed_backup_reminder_page.dart b/lib/new-ui/pages/seed/seed_backup_reminder_page.dart
new file mode 100644
index 0000000..d2558ba
--- /dev/null
+++ b/lib/new-ui/pages/seed/seed_backup_reminder_page.dart
@@ -0,0 +1,71 @@
+import "package:cake_wallet/core/auth_service.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/pages/seed/dismiss_seed_verification_page.dart";
+import "package:cake_wallet/new-ui/widgets/seed/seed_hero_image.dart";
+import "package:cake_wallet/new-ui/widgets/seed/seed_page_header.dart";
+import "package:cake_wallet/new-ui/widgets/seed/seed_page_scaffold.dart";
+import "package:cake_wallet/new-ui/widgets/stacked_buttons.dart";
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:flutter/cupertino.dart";
+import "package:flutter/material.dart";
+
+class SeedBackupReminderPage extends StatelessWidget {
+ const SeedBackupReminderPage({
+ required this.dashboardViewModel,
+ required this.authService,
+ super.key,
+ });
+
+ final DashboardViewModel dashboardViewModel;
+ final AuthService authService;
+
+ @override
+ Widget build(BuildContext context) {
+ final colors = Theme.of(context).colorScheme;
+
+ return SeedPageScaffold(
+ content: SeedPageHeader(
+ image: const SeedHeroImage(),
+ title: S.of(context).recovery_phrase_intro_title,
+ description: Text(
+ S.of(context).recovery_phrase_intro_description,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: colors.onSurfaceVariant,
+ letterSpacing: -0.07,
+ ),
+ ),
+ ),
+ footer: StackedButtons(
+ primaryKey: const ValueKey("seed_backup_reminder_page_button_key"),
+ primaryText: S.of(context).show_recovery_phrase,
+ onPrimary: () => authService.authenticateAction(
+ context,
+ route: Routes.seed,
+ arguments: false,
+ conditionToDetermineIfToUse2FA:
+ dashboardViewModel.settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
+ ),
+ secondaryKey: const ValueKey("seed_backup_reminder_page_dismiss_button_key"),
+ secondaryText: S.of(context).dismiss_this_step,
+ onSecondary: () => Navigator.of(context).push(
+ CupertinoPageRoute<void>(
+ builder: (_) => DismissSeedVerificationPage(onConfirm: () => _dismiss(context)),
+ ),
+ ),
+ secondaryAsLink: true,
+ ),
+ );
+ }
+
+ Future<void> _dismiss(BuildContext context) async {
+ await dashboardViewModel.dismissSeedBackupReminder();
+
+ if (!context.mounted) {
+ return;
+ }
+
+ Navigator.of(context).popUntil((route) => route.isFirst);
+ }
+}
diff --git a/lib/new-ui/widgets/coins_page/seed_backup_reminder_card.dart b/lib/new-ui/widgets/coins_page/seed_backup_reminder_card.dart
new file mode 100644
index 0000000..169440e
--- /dev/null
+++ b/lib/new-ui/widgets/coins_page/seed_backup_reminder_card.dart
@@ -0,0 +1,80 @@
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:flutter/material.dart";
+import "package:flutter_mobx/flutter_mobx.dart";
+
+class SeedBackupReminderCard extends StatelessWidget {
+ const SeedBackupReminderCard({required this.dashboardViewModel, required this.onTap, super.key});
+
+ final DashboardViewModel dashboardViewModel;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context) => Observer(
+ builder: (_) {
+ if (!dashboardViewModel.shouldShowSeedBackupReminder) {
+ return const SizedBox.shrink();
+ }
+
+ final colors = Theme.of(context).colorScheme;
+ final textTheme = Theme.of(context).textTheme;
+
+ return Padding(
+ padding: const EdgeInsets.only(left: 18, right: 18, top: 24),
+ child: MergeSemantics(
+ child: Semantics(
+ button: true,
+ child: GestureDetector(
+ onTap: onTap,
+ child: Container(
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: colors.surfaceContainer,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Row(
+ children: [
+ ExcludeSemantics(
+ child: CakeImageWidget(
+ imageUrl: "assets/new-ui/key.svg",
+ width: 24,
+ height: 24,
+ colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 4,
+ children: [
+ Text(
+ S.of(context).back_up_recovery_phrase,
+ style: textTheme.labelLarge?.copyWith(
+ color: colors.onSurface,
+ letterSpacing: -0.07,
+ ),
+ ),
+ Text(
+ S.of(context).back_up_recovery_phrase_description,
+ style: textTheme.bodySmall?.copyWith(
+ color: colors.onSurfaceVariant,
+ letterSpacing: -0.06,
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 8),
+ Icon(Icons.chevron_right, size: 16, color: colors.onSurfaceVariant),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+}
diff --git a/lib/router.dart b/lib/router.dart
index 256f92b..2131d6a 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -10,8 +10,8 @@ import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dar
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
-import 'package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart';
-import 'package:cake_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart';
+import "package:cake_wallet/new-ui/pages/seed/pre_seed_page.dart";
+import "package:cake_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
import "package:cake_wallet/new-ui/pages/receive_page.dart";
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import 'package:cake_wallet/new-ui/widgets/hardware_wallet/sync_key_images_sheet.dart';
diff --git a/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart b/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart
index ad64a2a..cb2fec1 100644
--- a/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart
+++ b/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart
@@ -1,9 +1,9 @@
import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/new-ui/pages/seed/open_wallet_after_seed_flow.dart';
+import "package:cake_wallet/new-ui/pages/seed/open_wallet_after_seed_flow.dart";
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
-import 'package:cake_wallet/view_model/wallet_seed_view_model.dart';
+import "package:cake_wallet/view_model/wallet_seed_view_model.dart";
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
Why this scored 15/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.