Skip to content

Commit b594627

Browse files
authored
Add backup account reminder notification system (#383)
* feat: add backup account reminder notification system - Implement backup reminder notification that appears first in notifications list - Trigger reminder on first app launch and new user creation - Dismiss reminder only when user views seed phrase - Integrate with existing notification system without breaking functionality - Persist reminder state across app restarts using SharedPreferences - Add animated notification bell with shake animation when backup needed * coderabbit suggestion * Add mounted check and improve bell icon
1 parent f250165 commit b594627

File tree

10 files changed

+277
-17
lines changed

10 files changed

+277
-17
lines changed

lib/features/auth/screens/register_screen.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
55
import 'package:mostro_mobile/core/app_theme.dart';
66
import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart';
77
import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart';
8+
import 'package:mostro_mobile/features/notifications/providers/backup_reminder_provider.dart';
89
import 'package:mostro_mobile/shared/widgets/custom_button.dart';
910
import 'package:mostro_mobile/shared/utils/nostr_utils.dart';
1011
import 'package:mostro_mobile/generated/l10n.dart';
@@ -30,7 +31,11 @@ class RegisterScreen extends HookConsumerWidget {
3031
ref.listen<AuthState>(authNotifierProvider, (previous, state) {
3132
if (state is AuthKeyGenerated) {
3233
privateKeyController.text = NostrUtils.nsecToHex(state.privateKey);
34+
// Show backup reminder when new key is generated
35+
ref.read(backupReminderProvider.notifier).showBackupReminder();
3336
} else if (state is AuthRegistrationSuccess) {
37+
// Show backup reminder on successful registration (for imported keys too)
38+
ref.read(backupReminderProvider.notifier).showBackupReminder();
3439
// Navigate to home after successful registration
3540
context.go('/');
3641
} else if (state is AuthFailure) {

lib/features/key_manager/key_management_screen.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart';
99
import 'package:mostro_mobile/features/key_manager/import_mnemonic_dialog.dart';
1010
import 'package:mostro_mobile/features/settings/settings_provider.dart';
1111
import 'package:mostro_mobile/features/restore/restore_manager.dart';
12+
import 'package:mostro_mobile/features/notifications/providers/backup_reminder_provider.dart';
1213
import 'package:mostro_mobile/shared/providers.dart';
1314
import 'package:mostro_mobile/generated/l10n.dart';
1415
import 'package:mostro_mobile/shared/providers/notifications_history_repository_provider.dart';
@@ -74,6 +75,9 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
7475
final keyManager = ref.read(keyManagerProvider);
7576
await keyManager.generateAndStoreMasterKey();
7677

78+
// Show backup reminder when generating new user
79+
ref.read(backupReminderProvider.notifier).showBackupReminder();
80+
7781
await _loadKeys();
7882
}
7983

@@ -283,6 +287,10 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
283287
setState(() {
284288
_showSecretWords = !_showSecretWords;
285289
});
290+
// Dismiss backup reminder when user views seed phrase
291+
if (_showSecretWords) {
292+
ref.read(backupReminderProvider.notifier).dismissBackupReminder();
293+
}
286294
},
287295
icon: Icon(
288296
_showSecretWords
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
import 'package:mostro_mobile/shared/providers/storage_providers.dart';
4+
5+
class BackupReminderNotifier extends StateNotifier<bool> {
6+
static const String _backupReminderKey = 'backup_reminder_dismissed';
7+
final SharedPreferencesAsync _prefs;
8+
9+
BackupReminderNotifier(this._prefs) : super(false) {
10+
_loadBackupReminderState();
11+
}
12+
13+
Future<void> _loadBackupReminderState() async {
14+
// If the backup reminder was dismissed, state should be false
15+
// If it was never dismissed (or new user), state should be true
16+
final isDismissed = await _prefs.getBool(_backupReminderKey) ?? false;
17+
state = !isDismissed;
18+
}
19+
20+
/// Shows the backup reminder (called on first app launch or new user creation)
21+
Future<void> showBackupReminder() async {
22+
await _prefs.setBool(_backupReminderKey, false);
23+
state = true;
24+
}
25+
26+
/// Dismisses the backup reminder (called when user views seed phrase)
27+
Future<void> dismissBackupReminder() async {
28+
await _prefs.setBool(_backupReminderKey, true);
29+
state = false;
30+
}
31+
32+
/// Checks if the backup reminder should be shown
33+
bool get shouldShowBackupReminder => state;
34+
}
35+
36+
final backupReminderProvider = StateNotifierProvider<BackupReminderNotifier, bool>((ref) {
37+
final prefs = ref.watch(sharedPreferencesProvider);
38+
return BackupReminderNotifier(prefs);
39+
});

lib/features/notifications/screens/notifications_screen.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
33
import 'package:heroicons/heroicons.dart';
44
import 'package:mostro_mobile/core/app_theme.dart';
55
import 'package:mostro_mobile/features/notifications/providers/notifications_provider.dart';
6+
import 'package:mostro_mobile/features/notifications/providers/backup_reminder_provider.dart';
67
import 'package:mostro_mobile/features/notifications/widgets/notification_item.dart';
78
import 'package:mostro_mobile/features/notifications/widgets/notifications_actions_menu.dart';
9+
import 'package:mostro_mobile/features/notifications/widgets/backup_reminder_notification.dart';
810
import 'package:mostro_mobile/shared/widgets/mostro_app_bar.dart';
911
import 'package:mostro_mobile/shared/widgets/notification_history_bell_widget.dart';
1012
import 'package:mostro_mobile/generated/l10n.dart';
@@ -15,6 +17,7 @@ class NotificationsScreen extends ConsumerWidget {
1517
@override
1618
Widget build(BuildContext context, WidgetRef ref) {
1719
final notifications = ref.watch(notificationsHistoryProvider);
20+
final shouldShowBackupReminder = ref.watch(backupReminderProvider);
1821

1922
return Scaffold(
2023
backgroundColor: AppTheme.backgroundDark,
@@ -37,7 +40,8 @@ class NotificationsScreen extends ConsumerWidget {
3740
),
3841
body: notifications.when(
3942
data: (notificationList) {
40-
if (notificationList.isEmpty) {
43+
// Show empty state only if no notifications AND no backup reminder
44+
if (notificationList.isEmpty && !shouldShowBackupReminder) {
4145
return Center(
4246
child: Column(
4347
mainAxisAlignment: MainAxisAlignment.center,
@@ -69,15 +73,29 @@ class NotificationsScreen extends ConsumerWidget {
6973
);
7074
}
7175

76+
// Calculate total items (backup reminder + regular notifications)
77+
final totalItems = (shouldShowBackupReminder ? 1 : 0) + notificationList.length;
78+
7279
return RefreshIndicator(
7380
onRefresh: () async {
7481
ref.invalidate(notificationsHistoryProvider);
7582
},
7683
child: ListView.builder(
7784
padding: AppTheme.mediumPadding,
78-
itemCount: notificationList.length,
85+
itemCount: totalItems,
7986
itemBuilder: (context, index) {
80-
final notification = notificationList[index];
87+
// Show backup reminder first if active
88+
if (shouldShowBackupReminder && index == 0) {
89+
return const Padding(
90+
padding: EdgeInsets.only(bottom: 8),
91+
child: BackupReminderNotification(),
92+
);
93+
}
94+
95+
// Adjust index for regular notifications
96+
final notificationIndex = shouldShowBackupReminder ? index - 1 : index;
97+
final notification = notificationList[notificationIndex];
98+
8199
return Padding(
82100
padding: const EdgeInsets.only(bottom: 8),
83101
child: NotificationItem(
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:go_router/go_router.dart';
4+
import 'package:heroicons/heroicons.dart';
5+
import 'package:mostro_mobile/core/app_theme.dart';
6+
import 'package:mostro_mobile/generated/l10n.dart';
7+
8+
class BackupReminderNotification extends ConsumerWidget {
9+
const BackupReminderNotification({super.key});
10+
11+
@override
12+
Widget build(BuildContext context, WidgetRef ref) {
13+
return Card(
14+
color: Theme.of(context).cardTheme.color?.withValues(alpha: 0.9),
15+
child: InkWell(
16+
onTap: () => _handleTap(context),
17+
borderRadius: _getCardBorderRadius(context),
18+
child: Padding(
19+
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
20+
child: Row(
21+
crossAxisAlignment: CrossAxisAlignment.start,
22+
children: [
23+
// Warning icon with red color
24+
Container(
25+
padding: const EdgeInsets.all(8),
26+
decoration: BoxDecoration(
27+
color: Colors.red.withValues(alpha: 0.1),
28+
borderRadius: BorderRadius.circular(8),
29+
),
30+
child: const HeroIcon(
31+
HeroIcons.exclamationTriangle,
32+
style: HeroIconStyle.solid,
33+
color: Colors.red,
34+
size: 20,
35+
),
36+
),
37+
const SizedBox(width: 12),
38+
// Notification content
39+
Expanded(
40+
child: Column(
41+
crossAxisAlignment: CrossAxisAlignment.start,
42+
children: [
43+
// Title
44+
Text(
45+
S.of(context)!.backupAccountReminder,
46+
style: const TextStyle(
47+
color: AppTheme.textPrimary,
48+
fontSize: 16,
49+
fontWeight: FontWeight.w600,
50+
height: 1.2,
51+
),
52+
),
53+
const SizedBox(height: 4),
54+
// Message
55+
Text(
56+
S.of(context)!.backupAccountReminderMessage,
57+
style: const TextStyle(
58+
color: AppTheme.textSecondary,
59+
fontSize: 14,
60+
height: 1.4,
61+
),
62+
),
63+
],
64+
),
65+
),
66+
const SizedBox(width: 8),
67+
// Navigation arrow
68+
const Icon(
69+
Icons.arrow_forward_ios,
70+
color: AppTheme.textSecondary,
71+
size: 16,
72+
),
73+
],
74+
),
75+
),
76+
),
77+
);
78+
}
79+
80+
BorderRadius _getCardBorderRadius(BuildContext context) {
81+
final shape = Theme.of(context).cardTheme.shape;
82+
if (shape is RoundedRectangleBorder) {
83+
return shape.borderRadius.resolve(Directionality.of(context));
84+
}
85+
return BorderRadius.circular(12);
86+
}
87+
88+
void _handleTap(BuildContext context) {
89+
// Navigate to Account/Key Management screen
90+
context.push('/key_management');
91+
}
92+
}

lib/features/walkthrough/screens/walkthrough_screen.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
44
import 'package:flutter_riverpod/flutter_riverpod.dart';
55
import 'package:mostro_mobile/generated/l10n.dart';
66
import 'package:mostro_mobile/features/walkthrough/providers/first_run_provider.dart';
7+
import 'package:mostro_mobile/features/notifications/providers/backup_reminder_provider.dart';
78
import 'package:mostro_mobile/features/walkthrough/utils/highlight_config.dart';
89

910
class WalkthroughScreen extends ConsumerStatefulWidget {
@@ -166,6 +167,8 @@ class _WalkthroughScreenState extends ConsumerState<WalkthroughScreen> {
166167

167168
Future<void> _onIntroEnd(BuildContext context) async {
168169
await ref.read(firstRunProvider.notifier).markFirstRunComplete();
170+
// Show backup reminder for first-time users
171+
ref.read(backupReminderProvider.notifier).showBackupReminder();
169172
if (context.mounted) {
170173
context.go('/');
171174
}

lib/l10n/intl_en.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1213,5 +1213,9 @@
12131213
"imageDecryptionError": "Error decrypting image",
12141214
"imageNotAvailable": "Image not available",
12151215
"couldNotOpenFile": "Could not open file",
1216-
"errorOpeningFile": "Error opening file"
1216+
"errorOpeningFile": "Error opening file",
1217+
1218+
"@_comment_backup_reminder": "Backup reminder notification",
1219+
"backupAccountReminder": "You must back up your account",
1220+
"backupAccountReminderMessage": "Back up your secret words to recover your account"
12171221
}

lib/l10n/intl_es.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,10 @@
11911191
"imageDecryptionError": "Error al desencriptar la imagen",
11921192
"imageNotAvailable": "Imagen no disponible",
11931193
"couldNotOpenFile": "No se pudo abrir el archivo",
1194-
"errorOpeningFile": "Error al abrir archivo"
1194+
"errorOpeningFile": "Error al abrir archivo",
1195+
1196+
"@_comment_backup_reminder": "Recordatorio de respaldo de cuenta",
1197+
"backupAccountReminder": "Debes respaldar tu cuenta",
1198+
"backupAccountReminderMessage": "Respalda tus palabras secretas para recuperar tu cuenta"
11951199

11961200
}

lib/l10n/intl_it.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1246,5 +1246,9 @@
12461246
"imageDecryptionError": "Errore nella decrittazione dell'immagine",
12471247
"imageNotAvailable": "Immagine non disponibile",
12481248
"couldNotOpenFile": "Impossibile aprire il file",
1249-
"errorOpeningFile": "Errore nell'apertura del file"
1249+
"errorOpeningFile": "Errore nell'apertura del file",
1250+
1251+
"@_comment_backup_reminder": "Promemoria backup account",
1252+
"backupAccountReminder": "Devi fare il backup del tuo account",
1253+
"backupAccountReminderMessage": "Salva le tue parole segrete per recuperare il tuo account"
12501254
}

0 commit comments

Comments
 (0)