Skip to content

fix(trades): cancel order updates status, unify detail screens - #97

Merged
grunch merged 4 commits into
mainfrom
fix/cancel-order-status-sync
Apr 9, 2026
Merged

fix(trades): cancel order updates status, unify detail screens#97
grunch merged 4 commits into
mainfrom
fix/cancel-order-status-sync

Conversation

@grunch

@grunch grunch commented Apr 9, 2026

Copy link
Copy Markdown
Member
  • Optimistic cancel: update trade DB to Canceled and remove from order book immediately in cancel_order(), don't wait for daemon response
  • Daemon Canceled handler also syncs status to DB for late responses
  • tradeStatusProvider falls back to trade DB when order is removed from the in-memory book, stops polling on terminal statuses
  • My Trades list navigates to MyOrderScreen instead of TradeDetailScreen so both Order Book and My Trades open the same detail screen
  • MyOrderScreen falls back to trade DB when order is not in order book
  • Invalidate rawTradesProvider after cancel so list reloads from DB
  • Resilient list_trades: skip rows with deserialization errors instead of failing the entire query
  • Retry button in trades screen invalidates rawTradesProvider too
  • Add pending status to TradeDetailScreen with cancel button

Summary by CodeRabbit

  • New Features

    • Added "Pending" status for published-but-unmatched orders
    • Cancel action available on pending orders; UI updates immediately and refreshes trades
  • Bug Fixes

    • Orders no longer disappear when removed from in-memory book — fallback to persisted trade data ensures status is shown
    • Persisted-trades reader skips malformed entries instead of failing
  • Improvements

    • Error logging/retry now refreshes raw trade data
    • Navigation routes correctly based on role and status; UI shows loading while resolving persisted fallback

- Optimistic cancel: update trade DB to Canceled and remove from order
  book immediately in cancel_order(), don't wait for daemon response
- Daemon Canceled handler also syncs status to DB for late responses
- tradeStatusProvider falls back to trade DB when order is removed from
  the in-memory book, stops polling on terminal statuses
- My Trades list navigates to MyOrderScreen instead of TradeDetailScreen
  so both Order Book and My Trades open the same detail screen
- MyOrderScreen falls back to trade DB when order is not in order book
- Invalidate rawTradesProvider after cancel so list reloads from DB
- Resilient list_trades: skip rows with deserialization errors instead
  of failing the entire query
- Retry button in trades screen invalidates rawTradesProvider too
- Add pending status to TradeDetailScreen with cancel button
@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@grunch has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 23 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 9 minutes and 23 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e751ea04-5efe-4433-909a-09fa0d6610f7

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff2ba0 and c8b5f69.

📒 Files selected for processing (1)
  • lib/features/trades/widgets/trades_list_item.dart

Walkthrough

Adds DB-backed fallbacks and terminal-state short-circuiting to order status polling; performs optimistic local DB sync on cancels (user and daemon paths); improves resilience when reading persisted trades; updates UI/providers/navigation to use persisted trade info and invalidate caches after cancels.

Changes

Cohort / File(s) Summary
Order state provider & screen
lib/features/order/providers/trade_state_provider.dart, lib/features/order/screens/my_order_screen.dart
tradeStatusProvider now stops polling when a terminal OrderStatus is observed and falls back to orders_api.listTrades() when getOrder() returns null; MyOrderScreen reads tradeInfoProvider as fallback to construct OrderItem and invalidates rawTradesProvider after cancel.
Trades UI & navigation
lib/features/trades/screens/trade_detail_screen.dart, lib/features/trades/screens/trades_screen.dart, lib/features/trades/widgets/trades_list_item.dart
Added TradeStatus.pending and mapped OrderStatus.pendingTradeStatus.pending; added pending CTA (CANCEL) that invalidates rawTradesProvider; improved error logging and changed retry to invalidate both rawTradesProvider and filteredTradesWithOrderStateProvider; TradesListItem tap routing now depends on trade.role and trade.status.
Rust orders API (logic + optimistic sync)
rust/src/api/orders.rs
After publishing cancel, perform optimistic local update: remove order from in-memory order_book and persist OrderStatus::Canceled to local trades DB (best-effort with warning logs); daemon Action::Canceled path also syncs canceled status into DB.
Rust DB deserialization resilience
rust/src/db/sqlite.rs
list_trades now selects id and data, deserializes rows in a loop, logs and skips rows with invalid JSON instead of failing the entire call, and returns successfully deserialized TradeInfo entries.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client (UI)
    participant Provider as State Provider
    participant DartAPI as Orders API (Dart bridge)
    participant MemBook as In-Memory Order Book
    participant DB as Local Trades DB
    participant Daemon as Daemon/Gift-Wrap

    rect rgba(100,200,100,0.5)
    Note over UI,DB: Fallback lookup when order missing
    UI->>DartAPI: getOrder(orderId)
    DartAPI->>MemBook: lookup order
    MemBook-->>DartAPI: null
    DartAPI-->>Provider: null
    Provider->>DartAPI: listTrades()
    DartAPI->>DB: query trades
    DB-->>DartAPI: trades list
    DartAPI->>Provider: matching tradeInfo
    Provider->>Provider: _isTerminal(status)?
    Provider-->>UI: emit status (stop polling if terminal)
    end

    rect rgba(200,100,100,0.5)
    Note over UI,DB: Optimistic cancel flow
    UI->>DartAPI: cancelOrder(orderId)
    DartAPI->>MemBook: publish cancel action
    MemBook-->>DartAPI: ack
    par optimistic update
        DartAPI->>MemBook: remove order
        DartAPI->>DB: write OrderStatus::Canceled (best-effort)
        DB-->>DartAPI: ok/warn
    and daemon sync
        Daemon->>MemBook: gift-wrap cancel rumor
        MemBook->>DB: sync canceled status
    end
    DartAPI-->>UI: cancel confirmed
    UI->>Provider: invalidate rawTradesProvider
    Provider->>DB: reload trades
    UI->>UI: show updated state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped from membook down to disk with cheer,

When orders vanish, old trades reappear.
A CANCEL hop, I jot it in my log,
Skip broken rows and clear the fog.
Now UI and DB both show statuses clear.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objectives: optimistic cancel updates status and unifies detail screens for orders. It directly reflects the PR's core changes across both Rust and Flutter layers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 fix/cancel-order-status-sync

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.

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/order/providers/trade_state_provider.dart`:
- Around line 37-49: When in-memory order info is present (the info branch in
the trade-state polling logic) we currently yield info.status but do not stop
polling; add a terminal-status check and early return there like we do after the
DB fallback. Specifically, after yielding info.status in the block that handles
the result from getOrder()/info, call _isTerminal(info.status) and return if
true so the stream stops polling for terminal statuses; keep the existing
fallback to orders_api.listTrades() and its terminal check unchanged.

In `@lib/features/order/screens/my_order_screen.dart`:
- Around line 83-89: The UI is treating the fallback DB call as "not found"
because tradeInfoProvider(widget.orderId).valueOrNull is null while loading;
change the logic to first read the AsyncValue (e.g. final tradeInfo =
ref.watch(tradeInfoProvider(widget.orderId))) and only consider the order
missing when tradeInfo is not loading and tradeInfo.valueOrNull is null;
otherwise, if tradeInfo is loading, avoid rendering the not-found scaffold (or
show a loading state) and when tradeInfo has data use
OrderItem.fromInfo(tradeInfo.value!.order) to populate order.

In `@lib/features/trades/screens/trade_detail_screen.dart`:
- Around line 445-460: The CANCEL button's handler (_cancelOrder) doesn't
refresh the trades cache, so update _cancelOrder to invalidate/refresh the same
provider used by MyOrderScreen (rawTradesProvider) after a successful cancel;
locate the _cancelOrder method in TradeDetailScreen and, on successful
cancellation, call the provider invalidation/refresh logic (the same
ref.invalidate(rawTradesProvider)/controller refresh used elsewhere) so the
cached trade list and filters are updated immediately.

In `@lib/features/trades/widgets/trades_list_item.dart`:
- Line 61: The tap handler always navigates to the creator-style order route
(context.push(AppRoute.myOrderPath(trade.orderId))) which hides taken-order
flows; change the onTap in trades_list_item.dart to branch on whether the trade
is taken (e.g. trade.takerId == currentUser.id or trade.isTaken / trade.status
== 'taken') and navigate to the appropriate taken-order route (or pass a
mode/flag into MyOrderScreen) so taken trades use the taken-order UI (e.g.
context.push(AppRoute.takenOrderPath(trade.orderId)) or
context.push(AppRoute.myOrderPath(trade.orderId, mode: OrderViewMode.taken))).
Ensure you obtain current user id from the existing auth/user context and keep
MyOrderScreen-compatible params.

In `@rust/src/api/orders.rs`:
- Around line 752-768: The optimistic-cancel path currently uses the
possibly-unreconciled local placeholder order_id (from order_book().remove_order
and the db.update_trade_fields call) which can publish a Cancel for the local
UUID and remove/mark the trade locally while the daemon still has the real
order; fix this by detecting unresolved local IDs before publishing or mutating
local state: translate or look up the canonical daemon UUID for order_id (or
block the cancel and return an error if reconciliation hasn’t happened) and only
call order_book().remove_order and db.update_trade_fields with the canonical ID
(also apply the same check to the Action::Canceled DB sync code referenced
around the Action::Canceled handling at lines ~926-942 so pre-reconciled rows
aren’t missed).
- Around line 756-768: The optimistic cancel path calls update_trade_fields via
crate::db::app_db::db() with OrderStatus::Canceled but the IndexedDB backend
currently stubs persistence for wasm, so web builds never persist canceled
status; update the wasm IndexedDB implementation to perform an async write using
indexed_db_futures (or alter update_trade_fields to branch on target_arch =
"wasm32" and call the indexed_db_futures-based persistence routine) so the
cancel status is persisted for the DB-backed fallback; apply the same fix to the
other occurrence around lines referencing the second update_trade_fields call
(also at the noted 930-942 region) so both cancel-status updates use
indexed_db_futures on web.
🪄 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: ec4f7d41-70a4-43cd-bd45-90cd4d51b9dd

📥 Commits

Reviewing files that changed from the base of the PR and between 9064e98 and 8437f68.

📒 Files selected for processing (7)
  • lib/features/order/providers/trade_state_provider.dart
  • lib/features/order/screens/my_order_screen.dart
  • lib/features/trades/screens/trade_detail_screen.dart
  • lib/features/trades/screens/trades_screen.dart
  • lib/features/trades/widgets/trades_list_item.dart
  • rust/src/api/orders.rs
  • rust/src/db/sqlite.rs

Comment thread lib/features/order/providers/trade_state_provider.dart
Comment thread lib/features/order/screens/my_order_screen.dart
Comment thread lib/features/trades/screens/trade_detail_screen.dart
Comment thread lib/features/trades/widgets/trades_list_item.dart Outdated
Comment thread rust/src/api/orders.rs
Comment thread rust/src/api/orders.rs
…taker routing

- Stop polling tradeStatusProvider when in-memory order has terminal status
- Show loading state in MyOrderScreen while trade DB query is in flight
- Invalidate rawTradesProvider after cancel in TradeDetailScreen
- Route taker trades to TradeDetailScreen instead of MyOrderScreen

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/trades/screens/trade_detail_screen.dart`:
- Around line 218-221: The UI is showing the pending text/Cancel CTA before the
async status resolves; update the branches that currently check for
TradeStatus.pending (and OrderStatus.pending) to first handle the provider
unresolved state: read tradeStatus via tradeStatusProvider(widget.orderId),
treat TradeStatus.loading (and error) as "not pending", and only render the
pending instruction and Cancel CTA when tradeStatus == TradeStatus.pending and
the provider is not loading/error (i.e., confirmed pending). Apply this change
to both locations where pending is checked (the branch around the current
pending text and the similar branch at the later block around lines 447-462).

In `@lib/features/trades/widgets/trades_list_item.dart`:
- Around line 61-65: The current onTap routing only checks trade.role and always
sends creator trades to MyOrderScreen; change routing to consider the trade
lifecycle/status as well: if trade.role == TradeRole.creator AND trade.status ==
TradeStatus.pending (or lifecycle == 'pending'), push
AppRoute.myOrderPath(trade.orderId); otherwise (creator in
active/fiat-sent/completed/disputed OR any taker), push
AppRoute.tradeDetailPath(trade.orderId). Update the onTap in
trades_list_item.dart to branch on both trade.role and trade.status (use the
existing trade.status or trade.lifecycle property and the
AppRoute.tradeDetailPath / AppRoute.myOrderPath helpers).
🪄 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: e6f69404-81ef-497c-b7ad-f8d4039483aa

📥 Commits

Reviewing files that changed from the base of the PR and between 8437f68 and 22807c8.

📒 Files selected for processing (4)
  • lib/features/order/providers/trade_state_provider.dart
  • lib/features/order/screens/my_order_screen.dart
  • lib/features/trades/screens/trade_detail_screen.dart
  • lib/features/trades/widgets/trades_list_item.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/order/providers/trade_state_provider.dart

Comment thread lib/features/trades/screens/trade_detail_screen.dart
  1. trade_detail_screen.dart:329-331 — Replaced valueOrNull ?? OrderStatus.pending with an explicit check on the AsyncValue state. Now uses TradeStatus.loading
  while the provider hasn't resolved, preventing the pending instruction text and CANCEL button from flashing before the real status is known.
  2. trades_list_item.dart:61-65 — Changed routing logic: only creator trades that are still pending go to MyOrderScreen; all other trades (creators in
  active/fiatSent/etc. and all takers) route to TradeDetailScreen which has the full buyer/seller action flow.

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/trades/widgets/trades_list_item.dart`:
- Around line 61-65: The routing condition currently checks the stale DB
snapshot field trade.status but the UI uses the live effectiveStatus (from
tradeStatusProvider), causing inconsistent navigation; update the onTap logic in
trades_list_item.dart to use the same effectiveStatus value used for the status
chip (the computed effectiveStatus from the tradeStatusProvider) instead of
trade.status so the ternary that chooses between
AppRoute.myOrderPath(trade.orderId) and AppRoute.tradeDetailPath(trade.orderId)
uses effectiveStatus == TradeStatusFilter.pending together with trade.role ==
TradeRole.creator; if effectiveStatus isn't in scope, fetch/derive it the same
way the chip does (via tradeStatusProvider) and reuse that variable in the onTap
check.
- Line 22: Update the doc comment above the TradesListItem widget to reflect the
conditional navigation behavior: state that tapping navigates to either
`/my_order/:orderId` or `/trade_detail/:orderId` depending on the current user's
role and the trade status (i.e., the same condition used in the widget's tap
handler/onTap logic inside TradesListItem). Mention the routing is decided in
the widget's onTap handler (or the method that performs navigation) so readers
know the navigation target is conditional.
🪄 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: f0ddea96-ec37-4a44-a375-b8b616393bac

📥 Commits

Reviewing files that changed from the base of the PR and between 22807c8 and 7ff2ba0.

📒 Files selected for processing (2)
  • lib/features/trades/screens/trade_detail_screen.dart
  • lib/features/trades/widgets/trades_list_item.dart

Comment thread lib/features/trades/widgets/trades_list_item.dart Outdated
Comment thread lib/features/trades/widgets/trades_list_item.dart
@grunch
grunch merged commit 287b8d2 into main Apr 9, 2026
1 check passed
@grunch
grunch deleted the fix/cancel-order-status-sync branch April 9, 2026 12:31
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