Skip to content

Commit f417201

Browse files
feat(backup): redesign backup settings screen with posture hero
The screen listed every backup action at equal weight, so the one that mattered was the reader's problem to find. It now answers the same question the reminder does, in three parts: the status rows state the facts, one hero names the single most useful action, and the menu holds everything else. The hero is urgent when nothing is backed up (and replaces the Start Backup row), asks a vault-only wallet for a physical backup, asks for a test once the physical one is over a year old, and is absent when the backup is fresh — no empty card, no heading over nothing. It derives its posture from the same domain code the reminder uses, so the two surfaces cannot disagree, but it reads the real tested dates rather than the snooze-aware verdict: the screen keeps saying "last tested 14 months ago" while the popup is quiet. It stays silent until the first load resolves, because every wallet looks unprotected before then. Rename the Recoverbull menu row to "Encrypted vault settings" to match the status row's vocabulary. The key is deleted and re-added rather than renamed: all 26 non-EN values still said "Recoverbull", and falling back to English reads better than a translation of retired vocabulary. Compose the three sections as separate builders so the fork can add its metadata backup status row and menu row without touching hero logic.
1 parent b1cac9f commit f417201

29 files changed

Lines changed: 408 additions & 201 deletions

lib/features/backup_settings/ui/screens/backup_settings_screen.dart

Lines changed: 225 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import 'package:bb_mobile/core/themes/app_theme.dart';
22
import 'package:bb_mobile/core/utils/build_context_x.dart';
3+
import 'package:bb_mobile/core/widgets/buttons/button.dart';
34
import 'package:bb_mobile/core/widgets/navbar/top_bar.dart';
45
import 'package:bb_mobile/core/widgets/settings_entry_item.dart';
6+
import 'package:bb_mobile/core/widgets/text/text.dart';
7+
import 'package:bb_mobile/features/backup_settings/domain/backup_health_reminder.dart';
58
import 'package:bb_mobile/features/backup_settings/presentation/backup_settings_failure_l10n.dart';
69
import 'package:bb_mobile/features/backup_settings/presentation/cubit/backup_settings_cubit.dart';
710
import 'package:bb_mobile/features/backup_settings/ui/backup_settings_router.dart';
811
import 'package:bb_mobile/features/backup_settings/ui/widgets/view_vault_key_warning_bottom_sheet.dart';
912
import 'package:bb_mobile/features/labels/labels_facade.dart';
13+
import 'package:bb_mobile/features/test_wallet_backup/public/test_wallet_backup_routes.dart';
1014
import 'package:bb_mobile/features/transactions/ui/transactions_router.dart';
1115
import 'package:bb_mobile/features/recoverbull/presentation/bloc.dart';
1216
import 'package:bb_mobile/features/recoverbull/router.dart';
@@ -16,6 +20,7 @@ import 'package:flutter/material.dart';
1620
import 'package:flutter_bloc/flutter_bloc.dart';
1721
import 'package:gap/gap.dart';
1822
import 'package:go_router/go_router.dart';
23+
import 'package:timeago/timeago.dart' as timeago;
1924

2025
class BackupSettingsScreen extends StatefulWidget {
2126
const BackupSettingsScreen({super.key});
@@ -34,6 +39,12 @@ class _BackupSettingsScreenState extends State<BackupSettingsScreen> {
3439
}
3540
}
3641

42+
/// Answers one question, top to bottom: if this phone vanished right now,
43+
/// could you get your money back?
44+
///
45+
/// The status rows state the facts, the hero names the single most useful
46+
/// action, and the menu holds everything else. Each section is built by its
47+
/// own function so a row can be added to one without disturbing the others.
3748
class _Screen extends StatelessWidget {
3849
const _Screen();
3950

@@ -49,6 +60,7 @@ class _Screen extends StatelessWidget {
4960
},
5061
child: BlocBuilder<BackupSettingsCubit, BackupSettingsState>(
5162
builder: (context, state) {
63+
final hero = _hero(state);
5264
return Scaffold(
5365
appBar: AppBar(
5466
forceMaterialTransparency: true,
@@ -65,20 +77,16 @@ class _Screen extends StatelessWidget {
6577
child: Column(
6678
crossAxisAlignment: .start,
6779
children: [
68-
const Padding(
69-
padding: EdgeInsets.symmetric(horizontal: 16),
70-
child: _BackupTestStatusWidget(),
80+
Padding(
81+
padding: const EdgeInsets.symmetric(horizontal: 16),
82+
child: Column(
83+
crossAxisAlignment: .start,
84+
children: _statusRows(context, state),
85+
),
7186
),
72-
const Gap(40),
73-
const _StartBackupButton(),
74-
if (state.lastEncryptedBackup != null)
75-
const _ViewVaultKeyButton(),
76-
if (state.lastEncryptedBackup != null ||
77-
state.lastPhysicalBackup != null)
78-
const _TestBackupButton(),
79-
const _RecoverBullSettingsButton(),
80-
const _Bip329LabelsButton(),
81-
const _TransactionHistoryButton(),
87+
if (hero != null) ...[const Gap(32), hero],
88+
const Gap(24),
89+
..._menuRows(state),
8290
],
8391
),
8492
),
@@ -89,89 +97,243 @@ class _Screen extends StatelessWidget {
8997
),
9098
);
9199
}
100+
101+
/// The glanceable facts. Insertion point for the fork's metadata backup
102+
/// status row: append one [_StatusRow] here.
103+
List<Widget> _statusRows(BuildContext context, BackupSettingsState state) => [
104+
_StatusRow(
105+
label: context.loc.backupSettingsPhysicalBackup,
106+
isTested: state.isDefaultPhysicalBackupTested,
107+
lastTestedAt: state.lastPhysicalBackup,
108+
),
109+
const Gap(15),
110+
_StatusRow(
111+
label: context.loc.backupSettingsEncryptedVault,
112+
isTested: state.isDefaultEncryptedBackupTested,
113+
),
114+
];
115+
116+
/// The single most important action right now, or nothing at all when the
117+
/// status rows already say everything worth saying.
118+
Widget? _hero(BackupSettingsState state) {
119+
// Every wallet looks unprotected until the first load resolves; an urgent
120+
// card must never flash on the way in.
121+
if (state.status != BackupSettingsStatus.success) return null;
122+
123+
final posture = BackupHealthPosture.of(
124+
isEncryptedVaultTested: state.isDefaultEncryptedBackupTested,
125+
isPhysicalBackupTested: state.isDefaultPhysicalBackupTested,
126+
);
127+
switch (posture) {
128+
case null:
129+
return const _ZeroBackupHero();
130+
case BackupHealthPosture.recoverbullOnly:
131+
return const _AddPhysicalBackupHero();
132+
case BackupHealthPosture.physicalOnly:
133+
case BackupHealthPosture.both:
134+
final lastTestedAt = state.lastPhysicalBackup;
135+
final isDue = isBackupReminderDue(
136+
anchor: lastTestedAt,
137+
now: DateTime.now(),
138+
interval: posture.reminderInterval,
139+
);
140+
return isDue ? _TestBackupHero(lastTestedAt: lastTestedAt) : null;
141+
}
142+
}
143+
144+
/// The settings rows. Insertion point for the fork's metadata backup menu
145+
/// row: add one [SettingsEntryItem] to this list.
146+
List<Widget> _menuRows(BackupSettingsState state) => [
147+
if (state.lastEncryptedBackup != null) const _ViewVaultKeyButton(),
148+
if (state.lastEncryptedBackup != null || state.lastPhysicalBackup != null)
149+
const _TestBackupButton(),
150+
const _EncryptedVaultSettingsButton(),
151+
const _Bip329LabelsButton(),
152+
const _TransactionHistoryButton(),
153+
];
92154
}
93155

94-
class _BackupTestStatusWidget extends StatelessWidget {
95-
const _BackupTestStatusWidget();
156+
class _StatusRow extends StatelessWidget {
157+
final String label;
158+
final bool isTested;
159+
final DateTime? lastTestedAt;
160+
161+
const _StatusRow({
162+
required this.label,
163+
required this.isTested,
164+
this.lastTestedAt,
165+
});
96166

97167
@override
98168
Widget build(BuildContext context) {
99-
return BlocBuilder<BackupSettingsCubit, BackupSettingsState>(
100-
builder: (context, state) {
101-
return Column(
102-
crossAxisAlignment: .start,
169+
final testedAt = lastTestedAt;
170+
return Column(
171+
crossAxisAlignment: .start,
172+
children: [
173+
Row(
103174
children: [
104-
_StatusRow(
105-
label: context.loc.backupSettingsPhysicalBackup,
106-
isTested: state.isDefaultPhysicalBackupTested,
107-
),
108-
const Gap(15),
109-
_StatusRow(
110-
label: context.loc.backupSettingsEncryptedVault,
111-
isTested: state.isDefaultEncryptedBackupTested,
175+
Text(label, style: context.font.bodyMedium),
176+
const Spacer(),
177+
Text(
178+
isTested
179+
? context.loc.backupSettingsTested
180+
: context.loc.backupSettingsNotTested,
181+
style: context.font.bodyMedium?.copyWith(
182+
color: isTested
183+
? context.appColors.success
184+
: context.appColors.error,
185+
),
112186
),
113187
],
114-
);
115-
},
188+
),
189+
if (isTested && testedAt != null)
190+
Text(
191+
context.loc.backupHealthLastTested(timeago.format(testedAt)),
192+
style: context.font.bodySmall?.copyWith(
193+
color: context.appColors.textMuted,
194+
),
195+
),
196+
],
116197
);
117198
}
118199
}
119200

120-
class _StatusRow extends StatelessWidget {
121-
final String label;
122-
final bool isTested;
201+
/// One card, one verb. Shared by every hero so the screen can only ever ask
202+
/// for one thing at a time.
203+
class _HeroCard extends StatelessWidget {
204+
final String title;
205+
final String body;
206+
final String? footnote;
207+
final String actionLabel;
208+
final VoidCallback onAction;
209+
final bool isUrgent;
123210

124-
const _StatusRow({required this.label, required this.isTested});
211+
const _HeroCard({
212+
required this.title,
213+
required this.body,
214+
required this.actionLabel,
215+
required this.onAction,
216+
this.footnote,
217+
this.isUrgent = false,
218+
});
125219

126220
@override
127221
Widget build(BuildContext context) {
128-
return Row(
129-
children: [
130-
Text(label, style: context.font.bodyMedium),
131-
const Spacer(),
132-
Text(
133-
isTested
134-
? context.loc.backupSettingsTested
135-
: context.loc.backupSettingsNotTested,
136-
style: context.font.bodyMedium?.copyWith(
137-
color: isTested
138-
? context.appColors.success
139-
: context.appColors.error,
222+
final accent = isUrgent
223+
? context.appColors.error
224+
: context.appColors.onSurface;
225+
return Container(
226+
width: double.infinity,
227+
padding: const EdgeInsets.all(16),
228+
decoration: BoxDecoration(
229+
color: context.appColors.surfaceContainer,
230+
border: Border.all(color: accent, width: isUrgent ? 2 : 1),
231+
borderRadius: BorderRadius.circular(2),
232+
),
233+
child: Column(
234+
crossAxisAlignment: .start,
235+
children: [
236+
BBText(
237+
title,
238+
style: context.font.titleMedium?.copyWith(fontWeight: .bold),
239+
color: accent,
140240
),
141-
),
142-
],
241+
const Gap(8),
242+
BBText(
243+
body,
244+
style: context.font.bodyMedium,
245+
color: context.appColors.onSurface,
246+
),
247+
if (footnote != null) ...[
248+
const Gap(4),
249+
BBText(
250+
footnote!,
251+
style: context.font.bodySmall,
252+
color: context.appColors.textMuted,
253+
),
254+
],
255+
const Gap(16),
256+
BBButton.big(
257+
label: actionLabel,
258+
onPressed: onAction,
259+
bgColor: accent,
260+
textColor: context.appColors.surface,
261+
),
262+
],
263+
),
143264
);
144265
}
145266
}
146267

147-
class _TestBackupButton extends StatelessWidget {
148-
const _TestBackupButton();
268+
class _ZeroBackupHero extends StatelessWidget {
269+
const _ZeroBackupHero();
149270

150271
@override
151272
Widget build(BuildContext context) {
152-
return SettingsEntryItem(
153-
icon: Icons.verified,
154-
title: context.loc.backupSettingsTestBackup,
155-
onTap: () => context.pushNamed(
273+
return _HeroCard(
274+
isUrgent: true,
275+
title: context.loc.backupSettingsHeroBackUpTitle,
276+
body: context.loc.backupSettingsHeroBackUpBody,
277+
actionLabel: context.loc.backupSettingsStartBackupAction,
278+
onAction: () => context.pushNamed(
156279
BackupSettingsSubroute.backupOptions.name,
157-
extra: BackupSettingsFlow.test,
280+
extra: BackupSettingsFlow.backup,
158281
),
159282
);
160283
}
161284
}
162285

163-
class _StartBackupButton extends StatelessWidget {
164-
const _StartBackupButton();
286+
class _AddPhysicalBackupHero extends StatelessWidget {
287+
const _AddPhysicalBackupHero();
288+
289+
@override
290+
Widget build(BuildContext context) {
291+
return _HeroCard(
292+
title: context.loc.backupHealthReminderTitle,
293+
body: context.loc.backupHealthRecoverbullOnlyBody,
294+
actionLabel: context.loc.backupHealthAddPhysicalBackupAction,
295+
onAction: () => context.pushNamed(
296+
TestWalletBackupRoute.testPhysicalBackupFlow.name,
297+
extra: TestPhysicalBackupFlow.backup,
298+
),
299+
);
300+
}
301+
}
302+
303+
class _TestBackupHero extends StatelessWidget {
304+
final DateTime? lastTestedAt;
305+
306+
const _TestBackupHero({required this.lastTestedAt});
307+
308+
@override
309+
Widget build(BuildContext context) {
310+
final testedAt = lastTestedAt;
311+
return _HeroCard(
312+
title: context.loc.backupHealthReminderTitle,
313+
body: context.loc.backupHealthTestBackupBody,
314+
footnote: testedAt == null
315+
? null
316+
: context.loc.backupHealthLastTested(timeago.format(testedAt)),
317+
actionLabel: context.loc.backupHealthTestBackupAction,
318+
onAction: () => context.pushNamed(
319+
TestWalletBackupRoute.testPhysicalBackupFlow.name,
320+
extra: TestPhysicalBackupFlow.verify,
321+
),
322+
);
323+
}
324+
}
325+
326+
class _TestBackupButton extends StatelessWidget {
327+
const _TestBackupButton();
165328

166329
@override
167330
Widget build(BuildContext context) {
168331
return SettingsEntryItem(
169-
icon: Icons.save_as,
170-
iconColor: context.appColors.primary,
171-
title: context.loc.backupSettingsStartBackup,
332+
icon: Icons.verified,
333+
title: context.loc.backupSettingsTestBackup,
172334
onTap: () => context.pushNamed(
173335
BackupSettingsSubroute.backupOptions.name,
174-
extra: BackupSettingsFlow.backup,
336+
extra: BackupSettingsFlow.test,
175337
),
176338
);
177339
}
@@ -228,16 +390,16 @@ class _Bip329LabelsButton extends StatelessWidget {
228390
}
229391
}
230392

231-
class _RecoverBullSettingsButton extends StatelessWidget {
232-
const _RecoverBullSettingsButton();
393+
class _EncryptedVaultSettingsButton extends StatelessWidget {
394+
const _EncryptedVaultSettingsButton();
233395

234396
@override
235397
Widget build(BuildContext context) {
236398
return SettingsEntryItem(
237399
icon: Icons.cloud_circle,
238400
iconColor: context.appColors.secondary,
239401
textColor: context.appColors.secondary,
240-
title: context.loc.backupSettingsRecoverBullSettings,
402+
title: context.loc.backupSettingsEncryptedVaultSettings,
241403
onTap: () {
242404
context.pushNamed(
243405
RecoverBullRoute.recoverbullFlows.name,

0 commit comments

Comments
 (0)