From c671c106d4600adad5c74f3e47794e5646ee7cd6 Mon Sep 17 00:00:00 2001 From: grunch Date: Tue, 31 Mar 2026 09:24:08 -0300 Subject: [PATCH 1/3] =?UTF-8?q?feat(phase17):=20account=20&=20identity=20m?= =?UTF-8?q?anagement=20=E2=80=94=20privacy=20mode=20toggle,=20generate=20n?= =?UTF-8?q?ew=20user,=20reputation=20gating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/features/account/providers/privacy_mode_provider.dart: PrivacyModeNotifier StateNotifierProvider wrapping set_privacy_mode() with TODO(bridge) for Phase 18+ FFI - account_screen.dart: wire privacy mode options (remove Opacity/Coming-soon stub); tapping Reputation/Full-Privacy Mode updates provider; Generate New User dialog updated with correct warning and post-confirm flow (showBackupReminder → walkthrough navigation) - take_order_screen.dart: watch privacyModeProvider; hide creator reputation card (rating, trade count, days active) when privacy mode is active --- .../providers/privacy_mode_provider.dart | 23 +++++++ .../account/screens/account_screen.dart | 67 ++++++++++--------- .../order/screens/take_order_screen.dart | 61 +++++++++-------- specs/004-mostro-p2p-client/tasks.md | 4 +- 4 files changed, 93 insertions(+), 62 deletions(-) create mode 100644 lib/features/account/providers/privacy_mode_provider.dart diff --git a/lib/features/account/providers/privacy_mode_provider.dart b/lib/features/account/providers/privacy_mode_provider.dart new file mode 100644 index 00000000..54039c66 --- /dev/null +++ b/lib/features/account/providers/privacy_mode_provider.dart @@ -0,0 +1,23 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// In-memory privacy mode flag. +/// +/// Wraps `get_privacy_mode()` / `set_privacy_mode()` from the Rust reputation +/// API. UI reads and writes go through this provider so widgets can react +/// to changes without polling. +/// +/// TODO(bridge): initialise from `get_privacy_mode()` once the bridge is wired +/// (Phase 18+). +final privacyModeProvider = StateNotifierProvider( + (ref) => PrivacyModeNotifier(), +); + +class PrivacyModeNotifier extends StateNotifier { + PrivacyModeNotifier() : super(false); + + /// Toggle privacy mode and propagate to the Rust layer. + void setPrivacyMode(bool enabled) { + state = enabled; + // TODO(bridge): call set_privacy_mode(enabled) via FFI (Phase 18+). + } +} diff --git a/lib/features/account/screens/account_screen.dart b/lib/features/account/screens/account_screen.dart index fcb54a24..6d6d287d 100644 --- a/lib/features/account/screens/account_screen.dart +++ b/lib/features/account/screens/account_screen.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro/core/app_routes.dart'; import 'package:mostro/core/app_theme.dart'; import 'package:mostro/features/account/providers/backup_reminder_provider.dart'; +import 'package:mostro/features/account/providers/privacy_mode_provider.dart'; /// Account screen — Route `/key_management`. /// @@ -21,9 +24,6 @@ class _AccountScreenState extends ConsumerState { List? _words; bool _loadingWords = false; - // Privacy mode — placeholder until settings provider is wired in Phase 6 - bool _privacyMode = false; - String _maskPhrase(List words) { if (words.length < 4) return words.join(' '); final first = words.take(2).join(' '); @@ -66,6 +66,7 @@ class _AccountScreenState extends ConsumerState { final cardBg = colors?.backgroundCard ?? const Color(0xFF1E2230); final inputBg = colors?.backgroundInput ?? const Color(0xFF252A3A); final textSec = colors?.textSecondary ?? const Color(0xFFB0B3C6); + final privacyMode = ref.watch(privacyModeProvider); return Scaffold( appBar: AppBar( @@ -176,33 +177,29 @@ class _AccountScreenState extends ConsumerState { style: theme.textTheme.bodySmall!.copyWith(color: textSec), ), const SizedBox(height: AppSpacing.md), - Opacity( - opacity: 0.5, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _PrivacyOption( - title: 'Reputation Mode', - subtitle: 'Standard privacy with reputation', - selected: !_privacyMode, - green: green, - onTap: null, - ), - const SizedBox(height: AppSpacing.sm), - _PrivacyOption( - title: 'Full Privacy Mode', - subtitle: 'Maximum anonymity', - selected: _privacyMode, - green: green, - onTap: null, - ), - ], - ), - ), - const SizedBox(height: AppSpacing.sm), - Text( - 'Coming soon', - style: theme.textTheme.bodySmall!.copyWith(color: textSec), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _PrivacyOption( + title: 'Reputation Mode', + subtitle: 'Standard privacy with reputation', + selected: !privacyMode, + green: green, + onTap: () => ref + .read(privacyModeProvider.notifier) + .setPrivacyMode(false), + ), + const SizedBox(height: AppSpacing.sm), + _PrivacyOption( + title: 'Full Privacy Mode', + subtitle: 'Maximum anonymity', + selected: privacyMode, + green: green, + onTap: () => ref + .read(privacyModeProvider.notifier) + .setPrivacyMode(true), + ), + ], ), ], ), @@ -286,8 +283,8 @@ class _AccountScreenState extends ConsumerState { builder: (_) => AlertDialog( title: const Text('Generate New User?'), content: const Text( - 'This will permanently replace your current identity. ' - 'Make sure you have backed up your current secret words.', + 'This will create a brand-new identity. Your current secret words ' + 'will no longer work — make sure they are backed up before continuing.', ), actions: [ TextButton( @@ -297,7 +294,11 @@ class _AccountScreenState extends ConsumerState { FilledButton( onPressed: () { Navigator.pop(context); - // TODO: wire to create_identity() Rust bridge in Phase 5. + // TODO(bridge): call create_identity() via FFI (Phase 18+). + ref + .read(backupReminderProvider.notifier) + .showBackupReminder(); + context.go(AppRoute.walkthrough); }, child: const Text('Continue'), ), diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 99cdbe7a..4408b118 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:mostro/core/app_routes.dart'; import 'package:mostro/core/app_theme.dart'; +import 'package:mostro/features/account/providers/privacy_mode_provider.dart'; import 'package:mostro/features/home/providers/home_order_providers.dart'; import 'package:mostro/features/order/widgets/range_amount_modal.dart'; import 'package:mostro/shared/utils/fiat_currencies.dart'; @@ -138,6 +139,7 @@ class _TakeOrderScreenState extends ConsumerState { final cardBg = colors?.backgroundCard ?? const Color(0xFF1E2230); final textSec = colors?.textSecondary ?? const Color(0xFFB0B3C6); final flags = ref.watch(currencyFlagsProvider); + final privacyMode = ref.watch(privacyModeProvider); if (order == null) { return Scaffold( @@ -247,34 +249,39 @@ class _TakeOrderScreenState extends ConsumerState { ), const SizedBox(height: AppSpacing.sm), - // Card 5: Creator reputation - _InfoCard( - color: cardBg, - child: Row( - children: [ - const Icon(Icons.star, size: 16, color: Colors.amber), - const SizedBox(width: AppSpacing.xs), - Text( - order.rating.toStringAsFixed(1), - style: theme.textTheme.bodyMedium, - ), - const SizedBox(width: AppSpacing.lg), - Icon(Icons.person_outline, size: 16, color: textSec), - const SizedBox(width: AppSpacing.xs), - Text( - '${order.tradeCount}', - style: theme.textTheme.bodyMedium, - ), - const SizedBox(width: AppSpacing.lg), - Icon(Icons.calendar_month_outlined, size: 16, color: textSec), - const SizedBox(width: AppSpacing.xs), - Text( - '${order.daysActive}d', - style: theme.textTheme.bodyMedium, - ), - ], + // Card 5: Creator reputation — hidden in full privacy mode. + // TODO(bridge): the rate_counterpart route should also be skipped + // when privacy mode is on (controlled via the trade flow, not here). + if (!privacyMode) ...[ + _InfoCard( + color: cardBg, + child: Row( + children: [ + const Icon(Icons.star, size: 16, color: Colors.amber), + const SizedBox(width: AppSpacing.xs), + Text( + order.rating.toStringAsFixed(1), + style: theme.textTheme.bodyMedium, + ), + const SizedBox(width: AppSpacing.lg), + Icon(Icons.person_outline, size: 16, color: textSec), + const SizedBox(width: AppSpacing.xs), + Text( + '${order.tradeCount}', + style: theme.textTheme.bodyMedium, + ), + const SizedBox(width: AppSpacing.lg), + Icon(Icons.calendar_month_outlined, size: 16, color: textSec), + const SizedBox(width: AppSpacing.xs), + Text( + '${order.daysActive}d', + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), + const SizedBox(height: AppSpacing.sm), + ], const SizedBox(height: AppSpacing.xl), // Countdown timer diff --git a/specs/004-mostro-p2p-client/tasks.md b/specs/004-mostro-p2p-client/tasks.md index 83f4f0b1..7ec18abd 100644 --- a/specs/004-mostro-p2p-client/tasks.md +++ b/specs/004-mostro-p2p-client/tasks.md @@ -373,8 +373,8 @@ configuration. **Independent Test**: Account screen → masked words → Show → 12 visible. Toggle privacy mode → subsequent trades use anonymous identity. Generate new user → backup reminder reactivates. -- [ ] T116 Extend account screen in `lib/features/account/screens/account_screen.dart`: add privacy mode toggle card ("Reputation mode" / "Full privacy mode") calling `set_privacy_mode()`. Add "Generate New User" option with confirmation dialog: warns this creates a new identity and old backup words won't work. On confirm → `create_identity()` (new mnemonic) → `showBackupReminder()` reactivates → navigate to `/walkthrough` or home with new red dot. -- [ ] T117 [P] Wire privacy mode display: when in Full Privacy mode, trade screens should not show reputation data (rating stars, review count). `orderNotifierProvider` respects privacy mode setting. Settings screen reflects current mode. +- [x] T116 Extend account screen in `lib/features/account/screens/account_screen.dart`: add privacy mode toggle card ("Reputation mode" / "Full privacy mode") calling `set_privacy_mode()`. Add "Generate New User" option with confirmation dialog: warns this creates a new identity and old backup words won't work. On confirm → `create_identity()` (new mnemonic) → `showBackupReminder()` reactivates → navigate to `/walkthrough` or home with new red dot. +- [x] T117 [P] Wire privacy mode display: when in Full Privacy mode, trade screens should not show reputation data (rating stars, review count). `orderNotifierProvider` respects privacy mode setting. Settings screen reflects current mode. **Checkpoint**: All 3 account actions work: view secret words (backup confirmed), toggle privacy mode (affects subsequent trades), generate new identity (backup reminder reactivates). From 8d8eec69d2d1c6932f90df9d636e3e7271a10d59 Mon Sep 17 00:00:00 2001 From: grunch Date: Tue, 31 Mar 2026 09:39:01 -0300 Subject: [PATCH 2/3] =?UTF-8?q?fix(phase17):=20apply=20code=20review=20?= =?UTF-8?q?=E2=80=94=20release-safe=20AppColors=20checks,=20copy=20feedbac?= =?UTF-8?q?k,=20web=20persistence=20warning,=20eventual-consistency=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/about/screens/about_screen.dart | 10 ++++++-- .../providers/privacy_mode_provider.dart | 2 +- .../chat/screens/chat_room_screen.dart | 6 ++--- .../chat/screens/chat_rooms_screen.dart | 3 +-- lib/features/chat/widgets/chat_list_item.dart | 3 +-- .../chat/widgets/encrypted_file_message.dart | 3 +-- .../chat/widgets/encrypted_image_message.dart | 3 +-- lib/features/chat/widgets/info_panels.dart | 6 ++--- lib/features/chat/widgets/message_bubble.dart | 6 ++--- lib/features/chat/widgets/message_input.dart | 3 +-- .../disputes/screens/dispute_chat_screen.dart | 3 +-- .../disputes/widgets/dispute_list_item.dart | 3 +-- .../widgets/dispute_message_input.dart | 3 +-- .../widgets/dispute_messages_list.dart | 3 +-- .../disputes/widgets/disputes_list.dart | 3 +-- .../providers/notifications_provider.dart | 5 ++++ .../rate/screens/rate_counterpart_screen.dart | 3 +-- .../settings/screens/log_report_screen.dart | 4 ++-- .../screens/notification_settings_screen.dart | 4 ++-- .../widgets/currency_selector_dialog.dart | 4 ++-- .../settings/widgets/language_selector.dart | 4 ++-- .../widgets/relay_management_card.dart | 4 ++-- .../trades/screens/trades_screen.dart | 3 +-- .../trades/widgets/trades_list_item.dart | 3 +-- lib/l10n/app_localizations.dart | 23 ++++++++++--------- rust/src/api/settings.rs | 3 +++ 26 files changed, 58 insertions(+), 62 deletions(-) diff --git a/lib/features/about/screens/about_screen.dart b/lib/features/about/screens/about_screen.dart index fd58a5a0..c0702295 100644 --- a/lib/features/about/screens/about_screen.dart +++ b/lib/features/about/screens/about_screen.dart @@ -32,6 +32,12 @@ class AboutScreen extends StatelessWidget { Clipboard.setData( const ClipboardData(text: 'https://mostro.network/docs'), ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Link copied to clipboard'), + duration: Duration(seconds: 2), + ), + ); }, ), ), @@ -41,8 +47,8 @@ class AboutScreen extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - final c = colors!; + if (colors == null) throw StateError('AppColors theme extension must be registered'); + final c = colors; return Scaffold( appBar: AppBar( diff --git a/lib/features/account/providers/privacy_mode_provider.dart b/lib/features/account/providers/privacy_mode_provider.dart index 54039c66..8ff5606c 100644 --- a/lib/features/account/providers/privacy_mode_provider.dart +++ b/lib/features/account/providers/privacy_mode_provider.dart @@ -15,7 +15,7 @@ final privacyModeProvider = StateNotifierProvider( class PrivacyModeNotifier extends StateNotifier { PrivacyModeNotifier() : super(false); - /// Toggle privacy mode and propagate to the Rust layer. + /// Set privacy mode to [enabled] and propagate to the Rust layer. void setPrivacyMode(bool enabled) { state = enabled; // TODO(bridge): call set_privacy_mode(enabled) via FFI (Phase 18+). diff --git a/lib/features/chat/screens/chat_room_screen.dart b/lib/features/chat/screens/chat_room_screen.dart index 0c9368ea..6266e239 100644 --- a/lib/features/chat/screens/chat_room_screen.dart +++ b/lib/features/chat/screens/chat_room_screen.dart @@ -130,8 +130,7 @@ class _ChatRoomScreenState extends ConsumerState { final room = _resolveRoom(); final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); return Scaffold( // Keyboard avoidance is handled manually via viewInsets.bottom padding @@ -227,8 +226,7 @@ class _AppBarTitle extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; return Column( diff --git a/lib/features/chat/screens/chat_rooms_screen.dart b/lib/features/chat/screens/chat_rooms_screen.dart index 9d3cb013..a997ad2f 100644 --- a/lib/features/chat/screens/chat_rooms_screen.dart +++ b/lib/features/chat/screens/chat_rooms_screen.dart @@ -18,8 +18,7 @@ class ChatRoomsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; return DefaultTabController( diff --git a/lib/features/chat/widgets/chat_list_item.dart b/lib/features/chat/widgets/chat_list_item.dart index ef9c34bc..f3d64285 100644 --- a/lib/features/chat/widgets/chat_list_item.dart +++ b/lib/features/chat/widgets/chat_list_item.dart @@ -23,8 +23,7 @@ class ChatListItem extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; final contextLine = room.isSelling diff --git a/lib/features/chat/widgets/encrypted_file_message.dart b/lib/features/chat/widgets/encrypted_file_message.dart index 2d9659ac..d12ff429 100644 --- a/lib/features/chat/widgets/encrypted_file_message.dart +++ b/lib/features/chat/widgets/encrypted_file_message.dart @@ -31,8 +31,7 @@ class _EncryptedFileMessageState extends State { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; final icon = _iconForMime(widget.mimeType); diff --git a/lib/features/chat/widgets/encrypted_image_message.dart b/lib/features/chat/widgets/encrypted_image_message.dart index 46a5895a..bf7d9b73 100644 --- a/lib/features/chat/widgets/encrypted_image_message.dart +++ b/lib/features/chat/widgets/encrypted_image_message.dart @@ -19,8 +19,7 @@ class EncryptedImageMessage extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; return GestureDetector( diff --git a/lib/features/chat/widgets/info_panels.dart b/lib/features/chat/widgets/info_panels.dart index 5a7cfdc8..f5f2b7c9 100644 --- a/lib/features/chat/widgets/info_panels.dart +++ b/lib/features/chat/widgets/info_panels.dart @@ -33,8 +33,7 @@ class TradeInformationTab extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; return Card( @@ -135,8 +134,7 @@ class UserInformationTab extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; return Card( diff --git a/lib/features/chat/widgets/message_bubble.dart b/lib/features/chat/widgets/message_bubble.dart index d0a297a2..4d084bd4 100644 --- a/lib/features/chat/widgets/message_bubble.dart +++ b/lib/features/chat/widgets/message_bubble.dart @@ -78,8 +78,7 @@ class MessageBubble extends StatelessWidget { } final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; final isMine = message.isMine; @@ -202,8 +201,7 @@ class _SystemMessage extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; return Padding( diff --git a/lib/features/chat/widgets/message_input.dart b/lib/features/chat/widgets/message_input.dart index ab4dc619..07969586 100644 --- a/lib/features/chat/widgets/message_input.dart +++ b/lib/features/chat/widgets/message_input.dart @@ -46,8 +46,7 @@ class _MessageInputState extends State { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); return Container( decoration: BoxDecoration( diff --git a/lib/features/disputes/screens/dispute_chat_screen.dart b/lib/features/disputes/screens/dispute_chat_screen.dart index 5189d2a9..1c703540 100644 --- a/lib/features/disputes/screens/dispute_chat_screen.dart +++ b/lib/features/disputes/screens/dispute_chat_screen.dart @@ -67,8 +67,7 @@ class _DisputeChatScreenState extends ConsumerState { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final dispute = ref.watch(disputeByIdProvider(widget.disputeId)); diff --git a/lib/features/disputes/widgets/dispute_list_item.dart b/lib/features/disputes/widgets/dispute_list_item.dart index c9058370..91259160 100644 --- a/lib/features/disputes/widgets/dispute_list_item.dart +++ b/lib/features/disputes/widgets/dispute_list_item.dart @@ -21,8 +21,7 @@ class DisputeListItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; final (statusBg, statusFg, statusLabel) = _statusChip(dispute.status); diff --git a/lib/features/disputes/widgets/dispute_message_input.dart b/lib/features/disputes/widgets/dispute_message_input.dart index 66de807e..569004fb 100644 --- a/lib/features/disputes/widgets/dispute_message_input.dart +++ b/lib/features/disputes/widgets/dispute_message_input.dart @@ -50,8 +50,7 @@ class _DisputeMessageInputState extends State { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final l10n = AppLocalizations.of(context); return Container( diff --git a/lib/features/disputes/widgets/dispute_messages_list.dart b/lib/features/disputes/widgets/dispute_messages_list.dart index 93df016e..75dac1c0 100644 --- a/lib/features/disputes/widgets/dispute_messages_list.dart +++ b/lib/features/disputes/widgets/dispute_messages_list.dart @@ -64,8 +64,7 @@ class _DisputeMessagesListState extends State { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); // Deduplicate by nostrEventId where present. final seen = {}; diff --git a/lib/features/disputes/widgets/disputes_list.dart b/lib/features/disputes/widgets/disputes_list.dart index 9e1badc0..4d40acbc 100644 --- a/lib/features/disputes/widgets/disputes_list.dart +++ b/lib/features/disputes/widgets/disputes_list.dart @@ -21,8 +21,7 @@ class DisputesList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final disputesAsync = ref.watch(userDisputeDataProvider); diff --git a/lib/features/notifications/providers/notifications_provider.dart b/lib/features/notifications/providers/notifications_provider.dart index f1512ad0..5c3af207 100644 --- a/lib/features/notifications/providers/notifications_provider.dart +++ b/lib/features/notifications/providers/notifications_provider.dart @@ -35,6 +35,11 @@ class SembastNotificationsStore { // lose all notifications on page reload (notificationsProviderWithDb // regresses to in-memory-only behavior). Fix: add sembast_web, switch // to databaseFactoryWeb, remove the sembast_memory import. + debugPrint( + '[notifications] WARNING: Using in-memory DB on web — ' + 'notifications will not persist across reloads. ' + 'Add sembast_web to pubspec.yaml to fix.', + ); db = await databaseFactoryMemory.openDatabase(_dbName); } else { final dir = await getApplicationDocumentsDirectory(); diff --git a/lib/features/rate/screens/rate_counterpart_screen.dart b/lib/features/rate/screens/rate_counterpart_screen.dart index 09985dc1..4fb39a2b 100644 --- a/lib/features/rate/screens/rate_counterpart_screen.dart +++ b/lib/features/rate/screens/rate_counterpart_screen.dart @@ -54,8 +54,7 @@ class _RateCounterpartScreenState @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; final green = colors.mostroGreen; diff --git a/lib/features/settings/screens/log_report_screen.dart b/lib/features/settings/screens/log_report_screen.dart index 7315840e..45be729c 100644 --- a/lib/features/settings/screens/log_report_screen.dart +++ b/lib/features/settings/screens/log_report_screen.dart @@ -71,8 +71,8 @@ class _LogReportScreenState extends ConsumerState { Widget build(BuildContext context) { final loggingEnabled = ref.watch(settingsProvider).loggingEnabled; final colorsRaw = Theme.of(context).extension(); - assert(colorsRaw != null, 'AppColors theme extension must be registered'); - final colors = colorsRaw!; + if (colorsRaw == null) throw StateError('AppColors theme extension must be registered'); + final colors = colorsRaw; return Scaffold( appBar: AppBar( diff --git a/lib/features/settings/screens/notification_settings_screen.dart b/lib/features/settings/screens/notification_settings_screen.dart index dc861671..d7078108 100644 --- a/lib/features/settings/screens/notification_settings_screen.dart +++ b/lib/features/settings/screens/notification_settings_screen.dart @@ -23,8 +23,8 @@ class _NotificationSettingsScreenState @override Widget build(BuildContext context) { final colorsRaw = Theme.of(context).extension(); - assert(colorsRaw != null, 'AppColors theme extension must be registered'); - final colors = colorsRaw!; + if (colorsRaw == null) throw StateError('AppColors theme extension must be registered'); + final colors = colorsRaw; return Scaffold( appBar: AppBar( diff --git a/lib/features/settings/widgets/currency_selector_dialog.dart b/lib/features/settings/widgets/currency_selector_dialog.dart index 5af1c42b..76190e92 100644 --- a/lib/features/settings/widgets/currency_selector_dialog.dart +++ b/lib/features/settings/widgets/currency_selector_dialog.dart @@ -97,8 +97,8 @@ class _CurrencySelectorDialogState @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - final c = colors!; + if (colors == null) throw StateError('AppColors theme extension must be registered'); + final c = colors; return Scaffold( appBar: AppBar( diff --git a/lib/features/settings/widgets/language_selector.dart b/lib/features/settings/widgets/language_selector.dart index 218e9dc4..bee17a47 100644 --- a/lib/features/settings/widgets/language_selector.dart +++ b/lib/features/settings/widgets/language_selector.dart @@ -28,8 +28,8 @@ class LanguageSelector extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentCode = ref.watch(settingsProvider).language; final colorsRaw = Theme.of(context).extension(); - assert(colorsRaw != null, 'AppColors theme extension must be registered'); - final colors = colorsRaw!; + if (colorsRaw == null) throw StateError('AppColors theme extension must be registered'); + final colors = colorsRaw; return SafeArea( child: Column( diff --git a/lib/features/settings/widgets/relay_management_card.dart b/lib/features/settings/widgets/relay_management_card.dart index 255bb3b7..6e38a979 100644 --- a/lib/features/settings/widgets/relay_management_card.dart +++ b/lib/features/settings/widgets/relay_management_card.dart @@ -136,8 +136,8 @@ class _RelayManagementCardState extends ConsumerState { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - final c = colors!; + if (colors == null) throw StateError('AppColors theme extension must be registered'); + final c = colors; return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/trades/screens/trades_screen.dart b/lib/features/trades/screens/trades_screen.dart index e39204b5..38669a06 100644 --- a/lib/features/trades/screens/trades_screen.dart +++ b/lib/features/trades/screens/trades_screen.dart @@ -18,8 +18,7 @@ class TradesScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final trades = ref.watch(filteredTradesWithOrderStateProvider); final selectedFilter = ref.watch(selectedStatusFilterProvider); diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index 5b2f0f1a..21dd6eb1 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -26,8 +26,7 @@ class TradesListItem extends StatelessWidget { @override Widget build(BuildContext context) { final colors = Theme.of(context).extension(); - assert(colors != null, 'AppColors theme extension must be registered'); - if (colors == null) return const SizedBox.shrink(); + if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; final titleText = diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e8dc1f82..dbc8b8f3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -66,7 +66,7 @@ import 'app_localizations_it.dart'; /// property. abstract class AppLocalizations { AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -89,11 +89,11 @@ abstract class AppLocalizations { /// of delegates is preferred or required. static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ @@ -101,7 +101,7 @@ abstract class AppLocalizations { Locale('en'), Locale('es'), Locale('fr'), - Locale('it') + Locale('it'), ]; /// Application name @@ -330,8 +330,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) { } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.'); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); } diff --git a/rust/src/api/settings.rs b/rust/src/api/settings.rs index 006e14b0..08a34893 100644 --- a/rust/src/api/settings.rs +++ b/rust/src/api/settings.rs @@ -170,6 +170,9 @@ pub async fn set_default_lightning_address(address: Option) -> Result<() /// during synchronous tests) we fall back to a synchronous write; the /// broadcast notification is skipped in that path but the flag is always set. pub fn set_logging_enabled(enabled: bool) { + // Note: the async path is fire-and-forget (spawn); callers that call + // get_settings() immediately after may not yet see the updated flag + // (eventually consistent). The sync fallback applies the change inline. match tokio::runtime::Handle::try_current() { Ok(handle) => { handle.spawn(async move { From 79104b47a8d0d94fb8575d1d041759a50ad4ab95 Mon Sep 17 00:00:00 2001 From: grunch Date: Tue, 31 Mar 2026 09:48:52 -0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(phase17):=20apply=20code=20review=20rou?= =?UTF-8?q?nd=202=20=E2=80=94=20blocking=20write=20for=20logging=20flag,?= =?UTF-8?q?=20await=20backup=20reminder=20before=20navigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/account/screens/account_screen.dart | 5 +++-- rust/src/api/settings.rs | 11 +++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/features/account/screens/account_screen.dart b/lib/features/account/screens/account_screen.dart index 6d6d287d..5f3f59e8 100644 --- a/lib/features/account/screens/account_screen.dart +++ b/lib/features/account/screens/account_screen.dart @@ -292,12 +292,13 @@ class _AccountScreenState extends ConsumerState { child: const Text('Cancel'), ), FilledButton( - onPressed: () { + onPressed: () async { Navigator.pop(context); // TODO(bridge): call create_identity() via FFI (Phase 18+). - ref + await ref .read(backupReminderProvider.notifier) .showBackupReminder(); + if (!context.mounted) return; context.go(AppRoute.walkthrough); }, child: const Text('Continue'), diff --git a/rust/src/api/settings.rs b/rust/src/api/settings.rs index 08a34893..f5f395ff 100644 --- a/rust/src/api/settings.rs +++ b/rust/src/api/settings.rs @@ -167,8 +167,8 @@ pub async fn set_default_lightning_address(address: Option) -> Result<() /// /// When a Tokio runtime is available the update is dispatched asynchronously /// and the broadcast notification is sent. When there is no runtime (e.g. -/// during synchronous tests) we fall back to a synchronous write; the -/// broadcast notification is skipped in that path but the flag is always set. +/// during synchronous tests) we fall back to a blocking write; the broadcast +/// notification is skipped in that path but the flag is always set. pub fn set_logging_enabled(enabled: bool) { // Note: the async path is fire-and-forget (spawn); callers that call // get_settings() immediately after may not yet see the updated flag @@ -183,12 +183,7 @@ pub fn set_logging_enabled(enabled: bool) { Err(_) => { // No async runtime — update the flag synchronously. // Notification is intentionally skipped here (best-effort). - match store().settings.try_write() { - Ok(mut guard) => guard.logging_enabled = enabled, - Err(_) => eprintln!( - "[settings] set_logging_enabled({enabled}): lock contention, update dropped" - ), - } + store().settings.blocking_write().logging_enabled = enabled; } } }