Skip to content

feat(ui): apply Mostro UX redesign from design handoff - #106

Merged
grunch merged 1 commit into
mainfrom
feat/ux-redesign-design-handoff
Jun 13, 2026
Merged

feat(ui): apply Mostro UX redesign from design handoff#106
grunch merged 1 commit into
mainfrom
feat/ux-redesign-design-handoff

Conversation

@grunch

@grunch grunch commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Implements the UX redesign proposals from the Claude Design handoff bundle (Mostro UX Redesign EN) onto the existing screens. The bundle's design doc prioritized proposals #1, #4 and #5; proposals #2 (first-trade onboarding) and #9 (ephemeral identity explainer) were not mocked in the canvas and are intentionally out of scope.

Per proposal

  • Add specs #1 Explicit next actiontrade_detail_screen.dart rebuilt: one large state-dependent primary CTA (Add invoice / Pay hold invoice / Mark fiat sent / Confirm & release / Rate), waiting-on-counterpart states render a disabled spinner button, Cancel / Dispute / Release collapsed behind the app-bar ⋮ overflow menu, step timeline card, and a persistent chat chip (counterpart handle + unread badge) replacing the ghost CONTACT button.
  • plan: update artifacts for 15 states, range orders, and regenerate tasks #5 Timer with context — trade detail and take-order timers now label what expires and the consequence ("If it expires, the order is removed from the book. It won't affect your reputation."), with lime → amber (<10%) → red (<2%) color thresholds.
  • docs: add Mostro Protocol reference as critical foundation #3 Order book reasons — deterministic differentiator pills (⚡ Best premium / ⭐ Most reputable / 🆕 Just published), each awarded to a single winner per visible book; color-coded premium pill; 5-star row replaced by numeric reputation (★ 4.9 · 47 trades · 312 days).
  • chore: update Rust version requirement to latest stable (1.94+) #6 Chat sticky trade state — pinned header below the chat app bar: status pill, buying/selling sats, fiat amount, payment method, live countdown, "View order ↗".
  • docs: update DESIGN_SYSTEM.md with exact v1 visual specs #8 Grouped notifications — collapsible per-trade groups with latest event, expandable history, unread badges, "Go to trade →"; backup reminder restyled as an amber system banner; filter chips All / Disputes / System (Active/Completed not derivable from the stored model — skipped rather than shipping dead UI).
  • Add dark/light theme #4 Backup ritual — trigger bottom sheet ("Back up now" / "Remind me tomorrow" with persisted 24 h snooze), full-screen 12-word grid with paper warning, 3-random-word verification with decoys from the same mnemonic, persisted backed-up flag + "Backed up" chip on the account screen. Words live only in screen state and are nulled on dispose.
  • docs: add ARCHITECTURE.md - Rust/Dart boundary ADR #7 Create-order presets — Express (prefilled from the last successful trade, hidden when none exists), Conservative, Custom cards above the form + live preview footer. Sats estimate only for fixed-price orders (no exchange-rate source exists in the codebase).

Theme

New warningAmber token in AppColors; everything else uses existing tokens. No new dependencies, no .arb changes (new strings follow each file's existing hardcoded-English pattern).

Test plan

  • flutter analyze — clean (3 remaining infos are pre-existing in untouched files)
  • flutter test — passes
  • Manual walkthrough of an end-to-end trade on a test node (book → take → invoice → fiat sent → release → rate)
  • Verify backup ritual on a fresh profile (trigger sheet → words → verification → badge)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added multi-step backup ritual to verify and confirm account backup security
    • Backup reminders with "Remind me tomorrow" snooze functionality
    • "Reason to pick" badges highlight best premium, most reputable, and newly published orders
    • Order preset selector for quick setup with Express, Conservative, and Custom options
    • Grouped and filterable notifications organized by trade or dispute
    • Trade status header now visible in chat conversations
  • Refactor

    • Enhanced notification screen with improved layout and filtering capabilities

Implements the redesign proposals from the Claude Design handoff bundle
(Mostro UX Redesign EN):

- Active trade (#1+#5): single state-dependent primary CTA, cancel/dispute
  collapsed behind the app-bar overflow menu, persistent chat chip with
  unread badge, step timeline, contextual timer with consequence copy and
  lime/amber/red thresholds
- Take order (#5): contextual 'time to take this order' timer card,
  3-column creator reputation, headline summary
- Order book (#3): differentiator reason pills (best premium / most
  reputable / just published), color-coded premium pill, numeric
  reputation row
- Chat (#6): sticky trade-state header with status, amounts, method,
  live countdown and view-order link
- Notifications (#8): grouped by trade with collapsible event history,
  unread badges, go-to-trade action, system banner, filter chips
- Create order (#7): Express / Conservative / Custom presets with
  prefill from last successful trade and live preview footer
- Backup ritual (#4): trigger bottom sheet, 12-word grid with paper
  warning, 3-random-word verification, persisted backed-up state
- Theme: new warningAmber token
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR introduces a backup account verification ritual, order badging and list UI redesign, a sticky trade state header with live countdown, notifications refactoring with grouping, order preset selector, and a major trade detail screen state-machine refactor. It also extends the color theme with a warning amber token.

Changes

Account Backup and Verification Flow

Layer / File(s) Summary
Theme color token
lib/core/app_theme.dart
AppColors adds warningAmber color token with constructor parameter, field, copyWith, and lerp interpolation across dark/light themes.
Backup reminder provider state
lib/features/account/providers/backup_reminder_provider.dart
BackupReminderNotifier now computes active state excluding snooze windows; snoozeUntilTomorrow() defers reminders. New BackupCompletedNotifier tracks completion via SharedPreferences with legacy fallback.
Backup ritual verification screen
lib/features/account/screens/backup_ritual_screen.dart
3-step UI: Step 1 displays 12-word mnemonic in 2-column grid, Step 2 generates random 3-word challenge with shuffled 4-option multiple-choice per slot, Step 3 confirms and closes. Async mnemonic loading, validation, and completion persistence via provider notifiers.
Backup trigger sheet and account integration
lib/features/account/widgets/backup_trigger_sheet.dart, lib/features/account/screens/account_screen.dart
Bottom sheet with 3-step preview and two CTAs ("Back up now" → ritual screen, "Remind me tomorrow" → snooze). AccountScreen watches completion state, renders "Backup Ritual" banner and "Backed up" badge on Secret Words card, resets state on new/imported identities.

Order Browsing and Listing Enhancements

Layer / File(s) Summary
Order reason assignment provider
lib/features/home/providers/order_reason_provider.dart
OrderReason enum and deterministic computeOrderReasons() that assigns at most one reason per order: lowest premium, highest reputation (tie-broken by trade count), or newest (within 10-minute window). Exposed via orderReasonsProvider watching filteredOrdersProvider.
Order list item redesign
lib/features/home/widgets/order_list_item.dart, lib/features/home/screens/home_screen.dart
OrderListItem adds reason: OrderReason? parameter and restructures layout: top row shows reason/type pills and timestamp; second row aligns fiat and premium; rating row displays star, numeric rating, and trade/day counts; payment methods in later row. Premium coloring now threshold-based (green/amber/red). Generic _Pill widget replaces _StarRating.

Trade and Chat UI Updates

Layer / File(s) Summary
Trade state sticky header
lib/features/chat/widgets/trade_state_header.dart, lib/features/chat/screens/chat_room_screen.dart
New chatTradeOrderProvider family resolves order via live order book, fallback Rust API, and persisted trades. TradeStateHeader renders sticky header below app bar with status pill, amount, fiat, payment method, and optional countdown chip. _CountdownChip ticks per second, displays MM:SS or H:MM:SS remaining, hides when expired.

Order Creation and Taking

Layer / File(s) Summary
Order preset selector
lib/features/order/widgets/order_preset_selector.dart
OrderPreset enum (express, conservative, custom), selectedOrderPresetProvider state, and lastSuccessfulOrderProvider async source (filters/sorts successful trades). Widget renders three preset cards (Express only when prior order exists) and invokes onSelect callback with preset and optional express source.
AddOrderScreen preset integration and live preview
lib/features/order/screens/add_order_screen.dart
Integrates OrderPresetSelector, adds _applyPreset() to prefill form from selected preset (updating range, fiat code, payment methods, premium, pricing mode). Introduces _previewFooter that renders live amount preview, market-price + premium formatting, and "live for 24 h" suffix.
TakeOrderScreen UI updates
lib/features/order/screens/take_order_screen.dart
Action button text changed to "BUY THESE SATS" / "SELL SATS"; order description now includes conditional buyer/seller messaging and richer price/premium styling. Creator reputation replaced with 3-stat block using new _ReputationStat widget. Countdown refactored into contextual card with circular progress indicator and expiration messaging.

Notifications Refactoring

Layer / File(s) Summary
Notifications screen stateful refactor
lib/features/notifications/screens/notifications_screen.dart
Converts from ConsumerWidget to ConsumerStatefulWidget with internal _filter state. Body partitions notifications into system items and trade/dispute groups, sorts by timestamp, renders system banners (with backup reminder) and collapsible NotificationGroupCard groups via filter. Adds _FilterChipsRow, _FilterChip, and _BackupReminderBanner local widgets; updates _TypeIcon and _EmptyState styling.
Notification group and system widgets
lib/features/notifications/widgets/notification_group_card.dart, lib/features/notifications/widgets/system_notification_banner.dart
NotificationGroupCard (StatefulWidget) renders grouped notifications with header, latest event row, collapsible earlier-event list, and footer navigation. _EventRow displays individual notifications with tap/menu support; _EventOverflowMenu gates "Mark as read" to unread state. SystemNotificationBanner renders system notifications with amber icon, unread dot, and overflow menu. Shared relativeTime() formatter for relative timestamps.

Trade Detail Screen State-Machine Refactor

Layer / File(s) Summary
TradeDetailScreen state-machine UI rewrite
lib/features/trades/screens/trade_detail_screen.dart
Refactored to state-machine UI: derives typed TradeStatus from async provider, shows dynamic app-bar title (ACTIVE TRADE vs ORDER DETAILS), renders persistent chat chip during in-flight states. Content reorganized into state strip (headline, instruction, optional countdown with progress bar/color thresholds) plus optional step timeline and single primary CTA area (maps state/role to enabled action or waiting button). Adds _MenuAction enum for cancel/dispute/release, async handlers _openDispute() and _releaseOrder() with navigation branching, centralized helpers for status→copy/color/step mapping, and shared UI components _Pill and _ChatChip (shows handle/avatar/unread count).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • MostroP2P/app#72: Both PRs modify backup_reminder_provider.dart constructor initialization and SharedPreferences key handling for snooze/completion state tracking.
  • MostroP2P/app#53: Both PRs expand backup completion state persistence and wire it to UI badges and notification tracking.
  • MostroP2P/app#54: Main PR's order reason badges and OrderListItem redesign directly build on the home order-book UI components introduced in this PR.

Poem

A backup ritual blooms in three careful steps,
With badges to guide where reason sleeps.
Trade states tick down in the chat above,
While orders get preset with reasons we love. 🐰⏰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately captures the main objective: applying a UX redesign from a design handoff to the Mostro UI across multiple screens and features.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ux-redesign-design-handoff

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7abe653ad5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +99 to +100
await prefs.setInt(kBackupSnoozedUntilKey, until.millisecondsSinceEpoch);
state = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Schedule the snoozed reminder to reappear

When a user taps “Remind me tomorrow” and keeps the app process alive past the 24-hour snooze window, this only writes the future timestamp and sets state = false; nothing schedules a wake-up or re-checks the timestamp, so the backup badge/banner stays hidden until some later provider reload or explicit reminder action. Since the comment says the reminder reappears once the snooze elapses, add a timer/invalidation for until (and cancel it on confirm/reset) so long-running sessions restore the active reminder on time.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
lib/features/trades/screens/trade_detail_screen.dart (2)

77-80: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Drive the countdown from the trade timeout, not from a one-time local seed.

_loadExpiresAt() only runs in initState() and only asks getOrder() for expiresAt. When the order has already left the book, Lines 104-105 leave the UI on the default 15-minute timer, and when an expiry does exist, setting _totalCountdownSeconds to the current diff resets the lime/amber/red thresholds to 100% every time the screen opens. Pull the active phase timeout from the trade-side source and refresh it when the trade status changes so the timer stays accurate after reopen/resume and across state transitions.

Also applies to: 93-106, 676-684

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/trades/screens/trade_detail_screen.dart` around lines 77 - 80,
The countdown is being seeded once in initState via _loadExpiresAt() which calls
getOrder() and sets _totalCountdownSeconds from the current diff, causing wrong
defaults and threshold resets when reopening; change the logic so the countdown
is driven by the authoritative trade timeout/phase values from the trade object
(use trade.expiresAt or trade.timeout/phaseTimeout) instead of a one-time local
diff, refresh that value whenever the trade status/phase changes and on resume
(hook into whatever trade update callback or status stream you already have),
and stop resetting _totalCountdownSeconds to the instantaneous remaining diff on
screen open—set _totalCountdownSeconds from the configured phase timeout and
compute remainingSeconds from expiresAt for display so amber/lime/red thresholds
remain stable (update _loadExpiresAt, _startCountdown and the trade status
change handler to pull and apply the trade-side timeout).

416-436: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't assume buyer while the persisted role is still loading.

Lines 421-423 force isBuyer = true until tradeRoleFromDbProvider resolves, so reopened seller trades briefly render buyer copy and the wrong primary CTA. Derive the fallback from order once it is available, or keep the role-dependent UI in a neutral/loading state until the role resolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/trades/screens/trade_detail_screen.dart` around lines 416 - 436,
The code currently defaults isBuyer = true while tradeRoleFromDbProvider is
unresolved, causing seller trades to render as buyer; change the logic in the
trade detail screen where you compute isBuyer (using tradeRoleProvider and
tradeRoleFromDbProvider) so that if dbRole is null you first try to derive the
role from the live order (the local variable order from orderBookProvider) and
only if order is also unavailable set isBuyer to null/unknown (or keep a
neutral/loading state); update any downstream UI that reads isBuyer to handle a
nullable/unknown role (show loading/neutral CTA) instead of assuming true.
Reference symbols: tradeRoleProvider, tradeRoleFromDbProvider, isBuyer, order,
orderBookProvider.
lib/features/account/providers/backup_reminder_provider.dart (1)

2-2: 📐 Maintainability & Code Quality | 🟠 Major

SharedPreferences-backed backup reminder state violates the UI-layer persistence guideline

lib/features/account/providers/backup_reminder_provider.dart persists reminder/completed/snooze flags using shared_preferences, but lib/**/*.dart requires Sembast for UI-layer state persistence—migrate this state to Sembast (or document an explicit exception).

confirmBackupComplete() writes to SharedPreferences and sets state without awaiting load(). In the normal app boot path BackupReminderNotifier is constructed with an initialValue in lib/main.dart (so load() isn’t in-flight), but for any ProviderContainer/notifier created without that override, an outstanding load() could update state after confirmation—call await load() (or gate on _loaded) to remove the risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/account/providers/backup_reminder_provider.dart` at line 2, The
BackupReminderNotifier currently persists state using SharedPreferences and
updates state in confirmBackupComplete() without awaiting load(), violating the
UI-layer persistence guideline; migrate persistence from SharedPreferences to
the app's Sembast-backed storage (or add explicit justification/documentation if
SharedPreferences must be used) and update
BackupReminderNotifier/confirmBackupComplete() to await load() (or gate on the
notifier's _loaded flag) before writing persistence and setting state to avoid a
stale in-flight load overwriting the confirmation; locate
BackupReminderNotifier, confirmBackupComplete(), load(), and any
SharedPreferences usage in backup_reminder_provider.dart to replace persistence
calls with the Sembast API and add the await/gating logic.

Source: Coding guidelines

🧹 Nitpick comments (3)
lib/features/chat/widgets/trade_state_header.dart (1)

27-46: 🚀 Performance & Scalability | ⚡ Quick win

Reuse the cached trade lookup instead of calling listTrades() on each refresh.

Because this family watches tradeStatusProvider(orderId), every status refresh re-executes the fallback path. Once the order has left the book, Line 40 reloads the full trades list on each run even though tradeInfoProvider/rawTradesProvider already cache that lookup, which turns the sticky header into repeated bridge I/O for the same trade. Prefer the existing trades provider as the fallback source here.

♻️ Possible simplification
-    final trades = await orders_api.listTrades();
-    final trade = trades.where((t) => t.order.id == orderId).firstOrNull;
+    final trade = await ref.watch(tradeInfoProvider(orderId).future);
     if (trade != null) return _toOrderItem(trade.order);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/chat/widgets/trade_state_header.dart` around lines 27 - 46, The
fallback path in chatTradeOrderProvider currently calls orders_api.listTrades()
on each re-run (triggered by tradeStatusProvider(orderId)); replace that
external call with the cached provider lookup instead: read or watch the
existing tradeInfoProvider or rawTradesProvider (whichever holds the list of
trades) and find the trade by orderId, returning _toOrderItem(trade.order) if
found; keep the prior checks against orderBookProvider and preserve the
try/catch around the API call only if you still need a last-resort network
lookup, but prefer the cached provider to avoid repeated bridge I/O.
lib/features/notifications/widgets/notification_group_card.dart (1)

13-25: 🩺 Stability & Availability | ⚡ Quick win

Enforce the non-empty notifications contract at construction time.

The widget dereferences notifications.first; add an assert so misuse fails fast with a clear message instead of runtime RangeError.

Suggested patch
 class NotificationGroupCard extends StatefulWidget {
   const NotificationGroupCard({
     super.key,
     required this.notifications,
     required this.onMarkRead,
     required this.onDelete,
     required this.onTapNotification,
     required this.onGoToTrade,
     this.isDisputeGroup = false,
-  });
+  }) : assert(notifications.isNotEmpty, 'notifications must not be empty');

Also applies to: 40-43

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/notifications/widgets/notification_group_card.dart` around lines
13 - 25, The constructor for NotificationGroupCard must enforce that the
notifications list is non-empty to avoid RangeError when accessing
notifications.first; add an assert in the NotificationGroupCard constructor (the
const NotificationGroupCard(...) initializer) such as
assert(notifications.isNotEmpty, 'NotificationGroupCard requires a non-empty
notifications list') so misuse fails fast with a clear message; apply the same
assert where the other constructor/initializer for this widget handles
notifications (the other NotificationGroupCard constructor lines referenced
around 40-43) to keep behavior consistent.
lib/features/notifications/widgets/system_notification_banner.dart (1)

5-6: 📐 Maintainability & Code Quality | ⚡ Quick win

Extract relativeTime into a shared notifications utility module.

Importing another widget file just for a formatter couples two UI components unnecessarily. Move relativeTime to a shared utility (for example, lib/features/notifications/utils/relative_time.dart) and import it from both widgets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/notifications/widgets/system_notification_banner.dart` around
lines 5 - 6, The import of relativeTime from notification_group_card.dart
couples widgets; extract the relativeTime function into a shared notifications
utility module (e.g., create a relative_time utility module) and update both
widgets to import relativeTime from that new module; locate the current
relativeTime definition in notification_group_card.dart, move its implementation
into the new utility, export it, then replace the existing import with the new
utility import in system_notification_banner and notification_group_card.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/features/account/providers/backup_reminder_provider.dart`:
- Around line 105-111: confirmBackupComplete() can race with the
constructor-triggered load() and have its writes overwritten; ensure
confirmBackupComplete awaits the initial load completion before mutating
prefs/state by adding and using a private completion marker (e.g., a Future or
Completer like _loadComplete or _initCompleter) that the constructor assigns to
the load() call (e.g., _loadComplete = load()) and that load() completes when
finished; then at the start of confirmBackupComplete() await that marker before
touching SharedPreferences and updating state (references:
confirmBackupComplete, load, constructor initialization).

In `@lib/features/account/screens/backup_ritual_screen.dart`:
- Around line 70-73: The dispose() implementation only nulls _words but leaves
other mnemonic-derived state in memory; update dispose() to also clear or null
out _filled, _options, and _wrongPick so all mnemonic fragments are scrubbed
when the screen is disposed (identify the dispose() method in
backup_ritual_screen.dart and set those fields to safe empty values or null
before calling super.dispose()).

In `@lib/features/account/widgets/backup_trigger_sheet.dart`:
- Around line 183-188: The onPressed handler currently calls
snoozeUntilTomorrow() on backupReminderProvider.notifier and immediately closes
the sheet; change it to await the async snoozeUntilTomorrow() call (from
BackupReminderNotifier or the notifier returned by
backupReminderProvider.notifier) before calling Navigator.of(context).pop(), and
handle errors from the awaited call (e.g., show an error SnackBar or keep the
sheet open) so persistence failures don’t silently close the UI.
- Around line 161-164: Replace the direct Navigator push of BackupRitualScreen
(navigator.push(MaterialPageRoute(...))) with the app's GoRouter/AppRoute
navigation: call context.push or context.go using the centralized route for the
backup ritual (e.g., the AppRoute/GoRoute entry that maps to BackupRitualScreen,
such as AppRoute.backupRitual or the route name "backupRitual"), and remove the
MaterialPageRoute usage; ensure you have the go_router import and use the
route's name/path consistent with your AppRoute definitions.

In `@lib/features/home/widgets/order_list_item.dart`:
- Around line 153-154: Replace the hardcoded caption 'Market price' in
order_list_item.dart with a localized string from AppLocalizations: add a new
key (e.g., marketPrice) to your .arb files with translations, run the
localization generation, and update the Text widget in the OrderListItem (or the
Text instance showing 'Market price') to use
AppLocalizations.of(context).marketPrice (or the generated getter name) instead
of the literal string so the label is internationalized.

In `@lib/features/notifications/screens/notifications_screen.dart`:
- Around line 146-152: System notification taps are routing to the fallback
snackbar because systemItems lack orderId/disputeId; change the onTap passed to
SystemNotificationBanner to be conditional so it becomes a no-op when there is
no concrete destination. Update the loop where SystemNotificationBanner is
created (for final n in systemItems) to pass onTap: (n.orderId == null &&
n.disputeId == null) ? null : () => _handleTap(context, n) (or an empty closure
if the banner API requires a non-null callback) so taps only invoke _handleTap
when a real route exists; leave onMarkRead/onDelete as-is. Ensure
SystemNotificationBanner accepts a nullable onTap or adapt to accept an empty
closure.

In `@lib/features/order/screens/add_order_screen.dart`:
- Around line 115-123: _applyPreset currently only sets
selectedPaymentMethodsProvider when parsed methods are non-empty and never
clears customPaymentMethodProvider, so stale payment selections leak into
_submit; update _applyPreset to explicitly clear both
selectedPaymentMethodsProvider and customPaymentMethodProvider when the preset
yields no methods (and in the branch that sets methods ensure
customPaymentMethodProvider is cleared or set appropriately), referencing the
selectedPaymentMethodsProvider, customPaymentMethodProvider, and _submit
identifiers so the paymentMethod payload is always derived only from the current
preset or explicit user input.

---

Outside diff comments:
In `@lib/features/account/providers/backup_reminder_provider.dart`:
- Line 2: The BackupReminderNotifier currently persists state using
SharedPreferences and updates state in confirmBackupComplete() without awaiting
load(), violating the UI-layer persistence guideline; migrate persistence from
SharedPreferences to the app's Sembast-backed storage (or add explicit
justification/documentation if SharedPreferences must be used) and update
BackupReminderNotifier/confirmBackupComplete() to await load() (or gate on the
notifier's _loaded flag) before writing persistence and setting state to avoid a
stale in-flight load overwriting the confirmation; locate
BackupReminderNotifier, confirmBackupComplete(), load(), and any
SharedPreferences usage in backup_reminder_provider.dart to replace persistence
calls with the Sembast API and add the await/gating logic.

In `@lib/features/trades/screens/trade_detail_screen.dart`:
- Around line 77-80: The countdown is being seeded once in initState via
_loadExpiresAt() which calls getOrder() and sets _totalCountdownSeconds from the
current diff, causing wrong defaults and threshold resets when reopening; change
the logic so the countdown is driven by the authoritative trade timeout/phase
values from the trade object (use trade.expiresAt or trade.timeout/phaseTimeout)
instead of a one-time local diff, refresh that value whenever the trade
status/phase changes and on resume (hook into whatever trade update callback or
status stream you already have), and stop resetting _totalCountdownSeconds to
the instantaneous remaining diff on screen open—set _totalCountdownSeconds from
the configured phase timeout and compute remainingSeconds from expiresAt for
display so amber/lime/red thresholds remain stable (update _loadExpiresAt,
_startCountdown and the trade status change handler to pull and apply the
trade-side timeout).
- Around line 416-436: The code currently defaults isBuyer = true while
tradeRoleFromDbProvider is unresolved, causing seller trades to render as buyer;
change the logic in the trade detail screen where you compute isBuyer (using
tradeRoleProvider and tradeRoleFromDbProvider) so that if dbRole is null you
first try to derive the role from the live order (the local variable order from
orderBookProvider) and only if order is also unavailable set isBuyer to
null/unknown (or keep a neutral/loading state); update any downstream UI that
reads isBuyer to handle a nullable/unknown role (show loading/neutral CTA)
instead of assuming true. Reference symbols: tradeRoleProvider,
tradeRoleFromDbProvider, isBuyer, order, orderBookProvider.

---

Nitpick comments:
In `@lib/features/chat/widgets/trade_state_header.dart`:
- Around line 27-46: The fallback path in chatTradeOrderProvider currently calls
orders_api.listTrades() on each re-run (triggered by
tradeStatusProvider(orderId)); replace that external call with the cached
provider lookup instead: read or watch the existing tradeInfoProvider or
rawTradesProvider (whichever holds the list of trades) and find the trade by
orderId, returning _toOrderItem(trade.order) if found; keep the prior checks
against orderBookProvider and preserve the try/catch around the API call only if
you still need a last-resort network lookup, but prefer the cached provider to
avoid repeated bridge I/O.

In `@lib/features/notifications/widgets/notification_group_card.dart`:
- Around line 13-25: The constructor for NotificationGroupCard must enforce that
the notifications list is non-empty to avoid RangeError when accessing
notifications.first; add an assert in the NotificationGroupCard constructor (the
const NotificationGroupCard(...) initializer) such as
assert(notifications.isNotEmpty, 'NotificationGroupCard requires a non-empty
notifications list') so misuse fails fast with a clear message; apply the same
assert where the other constructor/initializer for this widget handles
notifications (the other NotificationGroupCard constructor lines referenced
around 40-43) to keep behavior consistent.

In `@lib/features/notifications/widgets/system_notification_banner.dart`:
- Around line 5-6: The import of relativeTime from notification_group_card.dart
couples widgets; extract the relativeTime function into a shared notifications
utility module (e.g., create a relative_time utility module) and update both
widgets to import relativeTime from that new module; locate the current
relativeTime definition in notification_group_card.dart, move its implementation
into the new utility, export it, then replace the existing import with the new
utility import in system_notification_banner and notification_group_card.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29788c1c-f602-4fc5-adf6-a00c61e5399b

📥 Commits

Reviewing files that changed from the base of the PR and between b5e4dca and 7abe653.

📒 Files selected for processing (17)
  • lib/core/app_theme.dart
  • lib/features/account/providers/backup_reminder_provider.dart
  • lib/features/account/screens/account_screen.dart
  • lib/features/account/screens/backup_ritual_screen.dart
  • lib/features/account/widgets/backup_trigger_sheet.dart
  • lib/features/chat/screens/chat_room_screen.dart
  • lib/features/chat/widgets/trade_state_header.dart
  • lib/features/home/providers/order_reason_provider.dart
  • lib/features/home/screens/home_screen.dart
  • lib/features/home/widgets/order_list_item.dart
  • lib/features/notifications/screens/notifications_screen.dart
  • lib/features/notifications/widgets/notification_group_card.dart
  • lib/features/notifications/widgets/system_notification_banner.dart
  • lib/features/order/screens/add_order_screen.dart
  • lib/features/order/screens/take_order_screen.dart
  • lib/features/order/widgets/order_preset_selector.dart
  • lib/features/trades/screens/trade_detail_screen.dart

Comment on lines 105 to +111
Future<void> confirmBackupComplete() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(kBackupReminderDismissedKey, true);
await prefs.setBool(kBackupCompletedKey, true);
await prefs.remove(kBackupSnoozedUntilKey);
state = false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent confirmBackupComplete() from racing with initial load().

Line 105 mutates persisted flags/state without awaiting load(). If constructor-triggered load() is still in flight, it can overwrite state after confirmation.

Suggested fix
   Future<void> confirmBackupComplete() async {
+    await load();
     final prefs = await SharedPreferences.getInstance();
     await prefs.setBool(kBackupReminderDismissedKey, true);
     await prefs.setBool(kBackupCompletedKey, true);
     await prefs.remove(kBackupSnoozedUntilKey);
     state = false;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/account/providers/backup_reminder_provider.dart` around lines
105 - 111, confirmBackupComplete() can race with the constructor-triggered
load() and have its writes overwritten; ensure confirmBackupComplete awaits the
initial load completion before mutating prefs/state by adding and using a
private completion marker (e.g., a Future or Completer like _loadComplete or
_initCompleter) that the constructor assigns to the load() call (e.g.,
_loadComplete = load()) and that load() completes when finished; then at the
start of confirmBackupComplete() await that marker before touching
SharedPreferences and updating state (references: confirmBackupComplete, load,
constructor initialization).

Comment on lines +70 to +73
void dispose() {
// Drop the mnemonic from memory as soon as the ritual is left.
_words = null;
super.dispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

dispose() clears _words but leaves other mnemonic-derived fields populated.

Line 72 nulls _words, but _filled, _options, and _wrongPick can still retain secret-word fragments. Clear them too during dispose for full in-memory scrubbing.

Suggested fix
   `@override`
   void dispose() {
     // Drop the mnemonic from memory as soon as the ritual is left.
     _words = null;
+    _challenge = const [];
+    _filled = [null, null, null];
+    _activeSlot = 0;
+    _options = const [];
+    _wrongPick = null;
     super.dispose();
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/account/screens/backup_ritual_screen.dart` around lines 70 - 73,
The dispose() implementation only nulls _words but leaves other mnemonic-derived
state in memory; update dispose() to also clear or null out _filled, _options,
and _wrongPick so all mnemonic fragments are scrubbed when the screen is
disposed (identify the dispose() method in backup_ritual_screen.dart and set
those fields to safe empty values or null before calling super.dispose()).

Comment on lines +161 to +164
navigator.push(
MaterialPageRoute<void>(
builder: (_) => const BackupRitualScreen(),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether BackupRitualScreen already has a go_router route entry.
rg -n --type=dart "BackupRitualScreen|GoRoute|AppRoute|go_router|context\.push|context\.go"

Repository: MostroP2P/app

Length of output: 14584


Switch BackupRitualScreen navigation to go_router

lib/features/account/widgets/backup_trigger_sheet.dart (lines 161-164) launches BackupRitualScreen via navigator.push(MaterialPageRoute(...)), bypassing the centralized GoRouter/AppRoute navigation used elsewhere (and required by the **/*.dart guidelines). Use the corresponding GoRoute/AppRoute and navigate with context.push/go instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/account/widgets/backup_trigger_sheet.dart` around lines 161 -
164, Replace the direct Navigator push of BackupRitualScreen
(navigator.push(MaterialPageRoute(...))) with the app's GoRouter/AppRoute
navigation: call context.push or context.go using the centralized route for the
backup ritual (e.g., the AppRoute/GoRoute entry that maps to BackupRitualScreen,
such as AppRoute.backupRitual or the route name "backupRitual"), and remove the
MaterialPageRoute usage; ensure you have the go_router import and use the
route's name/path consistent with your AppRoute definitions.

Source: Coding guidelines

Comment on lines +183 to +188
onPressed: () {
ref
.read(backupReminderProvider.notifier)
.snoozeUntilTomorrow();
Navigator.of(context).pop();
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Await snooze persistence before dismissing the sheet.

Lines 183-187 fire snoozeUntilTomorrow() and immediately pop. If persistence fails, the failure is unhandled and the UI still closes as if it succeeded.

Suggested fix
             TextButton(
-              onPressed: () {
-                ref
-                    .read(backupReminderProvider.notifier)
-                    .snoozeUntilTomorrow();
-                Navigator.of(context).pop();
+              onPressed: () async {
+                try {
+                  await ref
+                      .read(backupReminderProvider.notifier)
+                      .snoozeUntilTomorrow();
+                  if (context.mounted) Navigator.of(context).pop();
+                } catch (_) {
+                  if (!context.mounted) return;
+                  ScaffoldMessenger.of(context).showSnackBar(
+                    const SnackBar(
+                      content: Text('Could not snooze reminder. Please try again.'),
+                    ),
+                  );
+                }
               },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onPressed: () {
ref
.read(backupReminderProvider.notifier)
.snoozeUntilTomorrow();
Navigator.of(context).pop();
},
onPressed: () async {
try {
await ref
.read(backupReminderProvider.notifier)
.snoozeUntilTomorrow();
if (context.mounted) Navigator.of(context).pop();
} catch (_) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Could not snooze reminder. Please try again.'),
),
);
}
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/account/widgets/backup_trigger_sheet.dart` around lines 183 -
188, The onPressed handler currently calls snoozeUntilTomorrow() on
backupReminderProvider.notifier and immediately closes the sheet; change it to
await the async snoozeUntilTomorrow() call (from BackupReminderNotifier or the
notifier returned by backupReminderProvider.notifier) before calling
Navigator.of(context).pop(), and handle errors from the awaited call (e.g., show
an error SnackBar or keep the sheet open) so persistence failures don’t silently
close the UI.

Comment on lines +153 to +154
'Market price',
style: TextStyle(color: textSec, fontSize: 11),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Consider localizing the "Market price" caption.

The caption 'Market price' is hardcoded and should be added to AppLocalizations for proper internationalization support.

🌐 Proposed fix to use localized string
 Text(
-  'Market price',
+  l10n.marketPriceCaption,
   style: TextStyle(color: textSec, fontSize: 11),
 ),

You'll need to add the corresponding key to your .arb localization files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/home/widgets/order_list_item.dart` around lines 153 - 154,
Replace the hardcoded caption 'Market price' in order_list_item.dart with a
localized string from AppLocalizations: add a new key (e.g., marketPrice) to
your .arb files with translations, run the localization generation, and update
the Text widget in the OrderListItem (or the Text instance showing 'Market
price') to use AppLocalizations.of(context).marketPrice (or the generated getter
name) instead of the literal string so the label is internationalized.

Comment on lines +146 to +152
for (final n in systemItems) ...[
SystemNotificationBanner(
notification: n,
onMarkRead: () => notifier.markAsRead(n.id),
onDelete: () => notifier.delete(n.id),
onTap: () => _handleTap(context, n),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid wiring dead-end taps for non-routable system notifications.

System items are built from notifications without orderId/disputeId, so this onTap currently routes users into the fallback snackbar path. Make taps conditional (or no-op) unless a concrete destination exists.

Suggested patch
                   SystemNotificationBanner(
                     notification: n,
                     onMarkRead: () => notifier.markAsRead(n.id),
                     onDelete: () => notifier.delete(n.id),
-                    onTap: () => _handleTap(context, n),
+                    onTap: (n.orderId != null || n.disputeId != null)
+                        ? () => _handleTap(context, n)
+                        : null,
                   ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/notifications/screens/notifications_screen.dart` around lines
146 - 152, System notification taps are routing to the fallback snackbar because
systemItems lack orderId/disputeId; change the onTap passed to
SystemNotificationBanner to be conditional so it becomes a no-op when there is
no concrete destination. Update the loop where SystemNotificationBanner is
created (for final n in systemItems) to pass onTap: (n.orderId == null &&
n.disputeId == null) ? null : () => _handleTap(context, n) (or an empty closure
if the banner API requires a non-null callback) so taps only invoke _handleTap
when a real route exists; leave onMarkRead/onDelete as-is. Ensure
SystemNotificationBanner accepts a nullable onTap or adapt to accept an empty
closure.

Comment on lines +115 to +123
final methods = source.paymentMethod
.split(',')
.map((m) => m.trim())
.where((m) => m.isNotEmpty)
.toList();
if (methods.isNotEmpty) {
ref.read(selectedPaymentMethodsProvider.notifier).state = methods;
}
ref.read(isMarketPriceProvider.notifier).state = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear stale payment method state when applying the Express preset

_applyPreset only sets selectedPaymentMethodsProvider when parsed methods are non-empty and never clears customPaymentMethodProvider. As a result, previous custom/selected methods can leak into _submit and produce an incorrect paymentMethod payload.

Proposed fix
         final methods = source.paymentMethod
             .split(',')
             .map((m) => m.trim())
             .where((m) => m.isNotEmpty)
             .toList();
-        if (methods.isNotEmpty) {
-          ref.read(selectedPaymentMethodsProvider.notifier).state = methods;
-        }
+        ref.read(selectedPaymentMethodsProvider.notifier).state = methods;
+        ref.read(customPaymentMethodProvider.notifier).state = '';
         ref.read(isMarketPriceProvider.notifier).state = true;
         ref.read(premiumValueProvider.notifier).state =
             source.premium.clamp(-10.0, 10.0);

Also applies to: 126-127, 151-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/order/screens/add_order_screen.dart` around lines 115 - 123,
_applyPreset currently only sets selectedPaymentMethodsProvider when parsed
methods are non-empty and never clears customPaymentMethodProvider, so stale
payment selections leak into _submit; update _applyPreset to explicitly clear
both selectedPaymentMethodsProvider and customPaymentMethodProvider when the
preset yields no methods (and in the branch that sets methods ensure
customPaymentMethodProvider is cleared or set appropriately), referencing the
selectedPaymentMethodsProvider, customPaymentMethodProvider, and _submit
identifiers so the paymentMethod payload is always derived only from the current
preset or explicit user input.

@grunch
grunch merged commit e23a9b7 into main Jun 13, 2026
1 check passed
@grunch
grunch deleted the feat/ux-redesign-design-handoff branch June 13, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant