Skip to content

fix(order): the take screen keeps Taking… while its own take settles - #456

Open
Catrya wants to merge 1 commit into
mainfrom
fix/take-screen-in-flight-unavailable
Open

fix(order): the take screen keeps Taking… while its own take settles#456
Catrya wants to merge 1 commit into
mainfrom
fix/take-screen-in-flight-unavailable

Conversation

@Catrya

@Catrya Catrya commented Sep 13, 2026

Copy link
Copy Markdown
Member

Closes #454.

After tapping Take order, the button switched from Taking… to "No longer available" for a few seconds before the app moved on to the trade. The take itself was fine, but the screen told the user the order was gone right when they got it.

Cause

take_order (Rust) updates the order's book entry with the trade's status as soon as the daemon confirms the take. It then persists the trade, subscribes to the order and installs the session, and only after that does it return. The take screen's build read that book change as the order leaving the pending book:

final isUnavailable =
    _cta == TakeOrderCta.unavailable ||
    live == null ||
    live.status != OrderStatus.pending;

Its own ref.listen just above already held back while a take was in flight (_cta != TakeOrderCta.loading), but this expression overrode it. The behaviour came with the take screen redesign (35c1aaf).

Change

While the take is loading, the button stays on Taking… whatever the book says:

final isUnavailable =
    _cta == TakeOrderCta.unavailable ||
    (_cta != TakeOrderCta.loading &&
        (live == null || live.status != OrderStatus.pending));

Once the take settles, nothing else changes:

  • Success: the screen navigates to the trade (or the bond, add-invoice or pay-invoice step), as before.
  • OrderAlreadyTaken: marks the order unavailable, as before.
  • Any other failure: lands back on idle, where a book that has moved on shows No longer available, which is true then.

A take that parks at WaitingTakerBond never hit this: Rust leaves its book entry pending during the bond window.

Tests

In take_order_screen_test.dart, TakeOrderScreen taking group:

  • Keeps Taking… while its own take moves the order on: the book moves the order to waitingBuyerInvoice while the take is pending, and the button still reads Taking…. This test fails without the change.
  • A failed take over an order that moved on reads unavailable: the order goes in-progress, then the take fails with NoDaemonResponse, and the screen shows No longer available. This pins the behaviour kept after a failure.

flutter analyze is clean and flutter test passes (1114).

Manual test

Took two orders against a local mostrod. Each went straight from Taking… to the trade, with no No longer available in between.

Notes

After tapping Take order, the button switched to No longer available
for a few seconds before the app moved on to the trade (#454). Rust
updates the order's book entry with the trade's status as soon as the
daemon confirms the take, then persists the trade, subscribes to the
order and installs the session, and only then does take_order return.
The screen's build read that change as the order leaving the pending
book, although its own listener already held back while a take was in
flight.

The build now does the same: while the take is loading, the button
stays on Taking… whatever the book says. Once the take settles,
nothing else changes. A success navigates, OrderAlreadyTaken marks the
order unavailable, and any other failure lands back on idle, where a
book that has moved on shows No longer available, which is true then.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b1ecbc87-936e-4dec-aeab-19e0df1b2977


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.

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actionable comments posted: 3

The diagnosis is right and the new rule is the right one: take_order writes the daemon's status into the book entry before persist_confirmed_take, subscribe_single_order and install_session, so the in-flight take was reading its own book write as the order going away. The rule is not enforced on every path, though. The same "No longer available" still reaches the user after a successful take and when the order expires mid-take. Both are verified below, with a fix that keeps all 19 tests in this file green.

🧹 Verified-OK notes (4)
  • Rust claims in the description — confirmed on 2724772: the book entry takes the reply's status (order_book().upsert_order(info)) before persistence, the per-order subscription and the session install; and !waiting_bond skips that write, so a take parked at WaitingTakerBond keeps its entry pending. ✅
  • Failure pathfinally returns _cta to idle after a non-OrderAlreadyTaken failure, and the book decides again; the new second test pins it. ✅
  • CI green on the merge ref (Flutter, Rust, Web smoke); mergeable: MERGEABLE; no commit on main since this branch's base touches either file. ✅
  • Overlap with #448 — both edit take_order_screen_test.dart. The tests here sit inside an existing group, as the description says, but #448 replaces _pump's MaterialApp with a router harness, so expect a small conflict in the harness whichever lands second.
📜 Review details

Reviewed commit: 2724772
Files reviewed: 2 — lib/features/order/screens/take_order_screen.dart, test/features/order/screens/take_order_screen_test.dart
Profile: strict

How the findings were verified (throwaway worktree on 2724772, not pushed). A probe test pumps TakeOrderScreen under a GoRouter, as a seller so the success path makes no Rust settings call. The book is fed the way the real flow feeds it.

Probe This PR With the suggested fix
Take succeeds after the book moved to waitingPayment: frames showing No longer available while TakeOrderScreen is still mounted 19 frames (~300 ms), then lands on the pay step 0 frames, lands on the pay step
Order's expiresAt passes while the take is in flight Taking…No longer available stays Taking…
take_order_screen_test.dart (19 tests, including this PR's two) pass pass

_cta == TakeOrderCta.unavailable ||
live == null ||
live.status != OrderStatus.pending;
(_cta != TakeOrderCta.loading &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

A successful take still shows "No longer available", now during the navigation instead of before it.

The hold is keyed on _cta == TakeOrderCta.loading, but _onTakeOrder's finally still runs after the navigation:

      context.go(AppRoute.tradeDetailPath(widget.orderId));
      context.push(AppRoute.payInvoicePath(widget.orderId));
    } catch (e) { … } finally {
      if (mounted && _cta == TakeOrderCta.loading) {
        setState(() => _cta = TakeOrderCta.idle);   // still mounted: the route is animating out
      }
    }

context.go doesn't unmount this screen synchronously; it stays in the tree while the next route animates in. The finally puts _cta back on idle, so this expression lets the book decide again. The book already reads waitingPayment/waitingBuyerInvoice from the user's own take, so the outgoing page renders No longer available, and the countdown switches to closed as well since isClosed: isUnavailable.

Reproduced on this commit: the book moves to waitingPayment, the take then succeeds, and 19 consecutive 16 ms frames show No longer available with TakeOrderScreen still mounted before the app lands on the pay step. That is #454's symptom, shortened but not gone. The manual test in the description can easily miss it under the transition.

Suggested fix: once the take succeeded and the screen is navigating away, don't fall back to idle.

     setState(() => _cta = TakeOrderCta.loading);
+    var navigated = false;
     try {
       final trade = await ref.read(takeOrderActionProvider)(…);
       if (!mounted) return;
       …
       if (trade.order.status == OrderStatus.waitingTakerBond) {
+        navigated = true;
         context.go(AppRoute.tradeDetailPath(widget.orderId));
         context.push(AppRoute.payBondPath(widget.orderId));
       } else if (widget.isBuying) {
         final settings = await settings_api.getSettings();
         if (!mounted) return;
+        navigated = true;
         context.go(AppRoute.tradeDetailPath(widget.orderId));
         …
       } else {
+        navigated = true;
         context.go(AppRoute.tradeDetailPath(widget.orderId));
         context.push(AppRoute.payInvoicePath(widget.orderId));
       }
     } catch (e) { … } finally {
-      if (mounted && _cta == TakeOrderCta.loading) {
+      if (mounted && !navigated && _cta == TakeOrderCta.loading) {
         setState(() => _cta = TakeOrderCta.idle);
       }
     }

Set the flag after the buyer branch's await settings_api.getSettings(), as above. If that call throws, the screen never navigates and must still return to idle, or the button is stuck on Taking…. The variant I ran set the flag once before the branches; the success/expiry probes and all 19 tests in this file pass with it, and moving it past the only await in the buyer branch doesn't change the seller or bond paths they exercise.

🤖 Prompt for AI Agents
In lib/features/order/screens/take_order_screen.dart, _onTakeOrder: add `var navigated = false;`
before the try; set `navigated = true;` immediately before the first context.go(...) of each
success branch (in the buyer branch, after `await settings_api.getSettings()` and its mounted
check); in `finally`, only reset `_cta` to idle when `!navigated`. Add a routed widget test where
the book moves the order to waitingPayment, the take then succeeds (seller path), and assert that
"No longer available" is never found while TakeOrderScreen is still in the tree.

live.status != OrderStatus.pending;
(_cta != TakeOrderCta.loading &&
(live == null || live.status != OrderStatus.pending));
final cta = isUnavailable ? TakeOrderCta.unavailable : _cta;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

The countdown can still override an in-flight take.

This line trusts _cta == TakeOrderCta.unavailable, and _syncCountdown sets exactly that when expiresAt passes, without checking for a take in flight:

    if (left <= Duration.zero) {
      _remaining.value = Duration.zero;
      if (_cta != TakeOrderCta.unavailable) {          // true while loading
        setState(() => _cta = TakeOrderCta.unavailable);
      }

An order whose expiry falls inside the few seconds take_order takes flips from Taking… to No longer available mid-take (reproduced on this commit). The finally then leaves it there, since _cta is no longer loading. If the daemon accepted the take, the user is told the order is gone right before being navigated to it: the case this PR sets out to remove, on another path.

Suggested fix: the countdown only retires an idle button; a take in flight is settled by the daemon's answer.

-      if (_cta != TakeOrderCta.unavailable) {
+      if (_cta == TakeOrderCta.idle) {
         setState(() => _cta = TakeOrderCta.unavailable);
       }

Verified: with this change the expiry probe keeps Taking…, and the existing expiry tests still pass. After a failed take, finally returns to idle and this expression reads the expired book entry, so the button still dies in place.

await tester.pump();

expect(find.text('Taking…'), findsOneWidget);
expect(find.text('No longer available'), findsNothing);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🧪 Test Coverage | 🟡 Minor

Neither new test lets the take succeed after the book moved, which is where the symptom still shows.

The first test leaves the take unanswered, so it only proves the hold while loading. The second covers the failure path. The success path, with the book already moved, the take resolving and finally running while the screen is still mounted, is untested, and so is expiry during the take. Both paths still regress (see the comments above).

Suggested additions (both fail on this commit and pass with the suggested fixes):

testWidgets('a take that succeeds never reads unavailable on the way out', (
  tester,
) async {
  // Seller path under a router, so the navigation and the exit frames are real.
  // Book → waitingPayment, then the take resolves successfully.
  // Pump ~60 × 16 ms and fail if "No longer available" is found while
  // `find.byType(TakeOrderScreen)` is still non-empty; end on the pay step.
});

testWidgets('an order expiring mid-take keeps Taking…', (tester) async {
  var now = kFakeNow;
  await withClock(Clock(() => now), () async {
    // expiresAt: kFakeNow + 2 s; tap Take with the reply left pending;
    // advance `now` past expiry and pump 3 s; expect 'Taking…', not
    // 'No longer available'.
  });
});

The first needs the router harness #448 introduces, or a local one. Coordinate with that PR to avoid a second harness.

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.

Take screen says "No longer available" while the user's own take is being confirmed

2 participants