fix: context-aware buttons during cooperative cancellation#576
Conversation
- Cooperative cancel showed incorrect buttons because the FSM had no memory of the previous status - Sellers saw a release button when no fiat was sent (no-op), buyers lost the fiat-sent button from active status - Add 4 semantic actions: cooperativeCancelNoFiatByYou/ByPeer and cooperativeCancelFiatSentByYou/ByPeer - Add fiatWasSent flag to OrderState, set on fiatSent/fiatSentOk, persists through copyWith chains - updateWith() remaps generic protocol actions to semantic variants based on the flag - Restore manager reconstructs fiatWasSent from stored message history for cooperativelyCanceled and dispute orders - Covers full transition chain: active/fiatSent/dispute to cooperativelyCanceled - Update ORDER_STATUS_HANDLING.md with new action matrix, restore flow, and design rationale - Add 8 unit tests for remapping logic and flag persistence
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
WalkthroughThis PR introduces four semantic cooperative-cancel action variants that distinguish whether fiat was sent, adds a persisted Changes
Sequence Diagram(s)sequenceDiagram
participant Restore as RestoreManager
participant Store as MessageStore
participant Notifier as OrderNotifier
participant State as OrderState
Restore->>Store: load order messages (fiatSent/fiatSentOk?)
Store-->>Restore: messages list
Restore->>Notifier: notifier.setFiatWasSent() if messages contain fiatSent/fiatSentOk
Notifier->>State: state = state.copyWith(fiatWasSent: true)
Restore->>State: restore status = cooperativelyCanceled -> use cooperativeCancelInitiatedByPeer
State->>State: updateWith(reconstructed action) -> remap to cooperativeCancelFiatSentByPeer or cooperativeCancelNoFiatByPeer based on fiatWasSent
State-->>Notifier: updated state (actions matrix now variant-specific)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
lib/features/order/notifiers/order_notifier.dart (1)
150-156: MakesetFiatWasSent()idempotent to avoid redundant state emissions.Line 154 always emits a new state even if
fiatWasSentis alreadytrue. A quick guard keeps restore replays cheaper.♻️ Proposed tweak
void setFiatWasSent() { - if (mounted) { - state = state.copyWith(fiatWasSent: true); - } + if (!mounted || state.fiatWasSent) return; + state = state.copyWith(fiatWasSent: true); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/order/notifiers/order_notifier.dart` around lines 150 - 156, The setFiatWasSent method currently emits a new state unconditionally; change it to be idempotent by checking the current state's fiatWasSent flag and only calling state = state.copyWith(fiatWasSent: true) when mounted is true AND state.fiatWasSent is false so redundant state emissions are avoided (refer to setFiatWasSent and state.copyWith(fiatWasSent: ...)).lib/features/notifications/utils/notification_message_mapper.dart (1)
67-73: Consider centralizing cooperative-cancel action grouping to reduce duplication.Both
getTitleKey()andgetMessageKey()now repeat the same variant lists. A tiny helper keeps future additions safer.♻️ Refactor sketch
class NotificationMessageMapper { + static bool _isCooperativeCancelByYou(mostro.Action action) { + return action == mostro.Action.cooperativeCancelInitiatedByYou || + action == mostro.Action.cooperativeCancelNoFiatByYou || + action == mostro.Action.cooperativeCancelFiatSentByYou; + } + + static bool _isCooperativeCancelByPeer(mostro.Action action) { + return action == mostro.Action.cooperativeCancelInitiatedByPeer || + action == mostro.Action.cooperativeCancelNoFiatByPeer || + action == mostro.Action.cooperativeCancelFiatSentByPeer; + } + static String getTitleKey(mostro.Action action) { + if (_isCooperativeCancelByYou(action)) { + return 'notification_cooperative_cancel_initiated_by_you_title'; + } + if (_isCooperativeCancelByPeer(action)) { + return 'notification_cooperative_cancel_initiated_by_peer_title'; + } switch (action) { - case mostro.Action.cooperativeCancelInitiatedByYou: - case mostro.Action.cooperativeCancelNoFiatByYou: - case mostro.Action.cooperativeCancelFiatSentByYou: - return 'notification_cooperative_cancel_initiated_by_you_title'; - case mostro.Action.cooperativeCancelInitiatedByPeer: - case mostro.Action.cooperativeCancelNoFiatByPeer: - case mostro.Action.cooperativeCancelFiatSentByPeer: - return 'notification_cooperative_cancel_initiated_by_peer_title'; ... } } static String getMessageKey(mostro.Action action) { + if (_isCooperativeCancelByYou(action)) { + return 'notification_cooperative_cancel_initiated_by_you_message'; + } + if (_isCooperativeCancelByPeer(action)) { + return 'notification_cooperative_cancel_initiated_by_peer_message'; + } switch (action) { - case mostro.Action.cooperativeCancelInitiatedByYou: - case mostro.Action.cooperativeCancelNoFiatByYou: - case mostro.Action.cooperativeCancelFiatSentByYou: - return 'notification_cooperative_cancel_initiated_by_you_message'; - case mostro.Action.cooperativeCancelInitiatedByPeer: - case mostro.Action.cooperativeCancelNoFiatByPeer: - case mostro.Action.cooperativeCancelFiatSentByPeer: - return 'notification_cooperative_cancel_initiated_by_peer_message'; ... } } }Also applies to: 169-175
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/notifications/utils/notification_message_mapper.dart` around lines 67 - 73, Both getTitleKey() and getMessageKey() duplicate the same cooperative-cancel action lists; extract a single helper (e.g., isCooperativeCancel(Action) or a constant Set<Action> cooperativeCancelActions) that contains the variants cooperativeCancelNoFiatByYou, cooperativeCancelFiatSentByYou, cooperativeCancelInitiatedByPeer, cooperativeCancelNoFiatByPeer, cooperativeCancelFiatSentByPeer and use that helper from both getTitleKey and getMessageKey to centralize the grouping and avoid duplication.lib/features/order/models/order_state.dart (1)
426-434: Consider removing dead code entries for generic variants in active/fiatSent matrices.Since
updateWith()now remapscooperativeCancelInitiatedByYou/Peerto the semantic variants beforegetActions()is called, these entries underStatus.activeandStatus.fiatSentwill never be matched. They're harmless but could be removed for clarity.Also applies to: 443-453, 568-576, 584-592
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/order/models/order_state.dart` around lines 426 - 434, Remove unreachable action-list entries for Action.cooperativeCancelInitiatedByPeer and Action.cooperativeCancelInitiatedByYou from the active/fiatSent matrices since updateWith() now remaps those generic variants to their semantic counterparts before getActions() runs; locate the matrices under Status.active and Status.fiatSent in order_state.dart (also the similar blocks around the ranges noted) and delete the lines that list cooperativeCancelInitiatedByPeer/cooperativeCancelInitiatedByYou while leaving the semantic variants intact so behavior is unchanged and the dead entries are cleaned up.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lib/features/notifications/utils/notification_message_mapper.dart`:
- Around line 67-73: Both getTitleKey() and getMessageKey() duplicate the same
cooperative-cancel action lists; extract a single helper (e.g.,
isCooperativeCancel(Action) or a constant Set<Action> cooperativeCancelActions)
that contains the variants cooperativeCancelNoFiatByYou,
cooperativeCancelFiatSentByYou, cooperativeCancelInitiatedByPeer,
cooperativeCancelNoFiatByPeer, cooperativeCancelFiatSentByPeer and use that
helper from both getTitleKey and getMessageKey to centralize the grouping and
avoid duplication.
In `@lib/features/order/models/order_state.dart`:
- Around line 426-434: Remove unreachable action-list entries for
Action.cooperativeCancelInitiatedByPeer and
Action.cooperativeCancelInitiatedByYou from the active/fiatSent matrices since
updateWith() now remaps those generic variants to their semantic counterparts
before getActions() runs; locate the matrices under Status.active and
Status.fiatSent in order_state.dart (also the similar blocks around the ranges
noted) and delete the lines that list
cooperativeCancelInitiatedByPeer/cooperativeCancelInitiatedByYou while leaving
the semantic variants intact so behavior is unchanged and the dead entries are
cleaned up.
In `@lib/features/order/notifiers/order_notifier.dart`:
- Around line 150-156: The setFiatWasSent method currently emits a new state
unconditionally; change it to be idempotent by checking the current state's
fiatWasSent flag and only calling state = state.copyWith(fiatWasSent: true) when
mounted is true AND state.fiatWasSent is false so redundant state emissions are
avoided (refer to setFiatWasSent and state.copyWith(fiatWasSent: ...)).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 65f71174-9173-48b6-b493-7c31b8380ff5
📒 Files selected for processing (14)
docs/architecture/ORDER_STATUS_HANDLING.mdlib/data/models/enums/action.dartlib/data/models/notification.dartlib/features/notifications/utils/notification_data_extractor.dartlib/features/notifications/utils/notification_message_mapper.dartlib/features/notifications/widgets/notification_details.dartlib/features/notifications/widgets/notification_item.dartlib/features/order/models/order_state.dartlib/features/order/notifiers/abstract_mostro_notifier.dartlib/features/order/notifiers/order_notifier.dartlib/features/restore/restore_manager.dartlib/features/trades/screens/trade_detail_screen.dartlib/features/trades/widgets/mostro_message_detail_widget.darttest/features/order/models/order_state_dispute_auto_close_test.dart
There was a problem hiding this comment.
The fix is solid. The approach of using a fiatWasSent boolean flag tracked on OrderState and remapping generic protocol actions to semantic variants in updateWith() is clean and avoids conditional logic at the button-rendering layer.
Highlights:
- The flag persists correctly through all transitions via
copyWith(), including theactive → fiatSent → dispute → cooperativeCancelchain. - Restore path is correctly handled:
cooperativelyCancelednow usescooperativeCancelInitiatedByPeeras the generic restore action (instead of the terminalcooperativeCancelAccepted), and the restore manager reconstructsfiatWasSentfrom stored message history for bothcooperativelyCanceledanddisputeorders. - The FSM button matrix is now accurate for all 4 semantic variants on both seller and buyer sides.
- 8 unit tests cover the critical remapping cases, flag persistence, and the dispute chain.
- Docs in
ORDER_STATUS_HANDLING.mdare thorough and explain the design rationale well.
The previous behavior (seller seeing a release button with no hold invoice to release, buyer losing the fiatSent button) was a real UX bug and this fix handles all the edge cases correctly. Approved.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
fix #575
Summary by CodeRabbit
Bug Fixes
Notifications
Documentation
Tests