Commit 94e8c0f
feat(predict): Bottom Sheet - Try Again Toast for failed Payments cp-7.77.0 (#30167)
# PR: fix(predict): replace bet slip auto-reopen with auto-dismissing
Retry toast
> Suggested branch: `fix/bet-slip-auto-reopen-during-pwat`
> Suggested labels: `team-mobile-predict`, `needs-qa`
> Assignee: yourself
---
## **Description**
### Problem
When paying with any token (PWAT) for a Predict bet, the bet slip would
pop back up unexpectedly while the deposit was still in flight. The
"Prediction in progress" loading toast would appear, then the slip would
re-open over it (often before the deposit even confirmed on-chain), and
then stay stuck open after the order completed. This was confusing and
felt broken.
Root cause: the auto-reopen `useEffect` in
[`PredictPreviewSheetContext`](app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx)
— added in #29184 to surface inline error banners after background
failures — fired on **any** transient `activeOrder.error` value. The
PredictController briefly sets `error` during its internal retry paths
(`PredictController.ts:1277` and `:2300`) even on flows that ultimately
succeed, so the slip popped back up over toasts that were still
mid-loading. The reopened slip didn't close on `SUCCESS` either, because
the freshly-mounted `usePredictBuyActions` instance has
`didInitiateOrderRef = false` and skips the SUCCESS pop.
### Solution
Replaced the auto-reopen with a user-initiated reopen via an
auto-dismissing **Retry** toast. The toast lives ~3s; tapping Retry
within that window reopens the slip with the original market context and
the inline `order_failed` banner. If the user does nothing, the toast
fades out and `activeOrder.error` is automatically cleared so the next
slip open is a clean state (no stale banner flash).
```mermaid
sequenceDiagram
participant User
participant Slip as Bet slip
participant Ctrl as PredictController
participant Toast
User->>Slip: Confirm bet (PWAT)
Ctrl->>Ctrl: state -> DEPOSITING
Slip->>Slip: animate close
Toast->>User: "Prediction in progress" loading toast
alt Order succeeds
Ctrl-->>Toast: 'confirmed' event
Toast->>User: "Prediction placed" success toast
else Order fails
Ctrl-->>Toast: state.error transitions truthy
Toast->>User: auto-dismissing "Failed to place prediction" + Retry (inline)
opt User taps Retry within ~3s
Toast->>Toast: cancel auto-clear timer
Toast->>Slip: openBuySheet(lastBuyParams)
Slip->>User: bet slip reopens with inline order_failed banner
end
opt User does nothing
Toast->>Toast: auto-dismisses (~3s)
Toast->>Ctrl: clearOrderError()
end
end
```
### Key changes
- **Removed** the auto-reopen `useEffect` and `dismissedWithErrorRef`
from
[`PredictPreviewSheetContext.tsx`](app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx).
- **Added** a state-based trigger inside the provider that fires a toast
via `ToastService.showToast(...)` whenever `activeOrder.error`
transitions falsy → truthy AND the bottom-sheet flow is enabled AND the
slip is closed AND we have remembered buy params from a previous open.
This mirrors the original auto-reopen condition but surfaces a toast
instead of taking over the screen. Using state (not the controller's
`'failed'` event) avoids the timing race on `isBackgroundOrder` that the
event-based path is subject to.
- **Added** module-level `isPredictSheetProviderMounted()` so the legacy
event-based toast in
[`usePredictToastRegistrations.tsx`](app/components/UI/Predict/hooks/usePredictToastRegistrations.tsx)
can suppress itself when the provider is mounted (avoids a duplicate
failure toast).
- **Added** a `clearErrorTimerRef` 3-second timer that calls
`clearOrderError()` after the toast auto-dismisses, so an unhandled
failure doesn't leave a stale `activeOrder.error` for the next slip
open. The timer is cancelled when the user taps Retry (so the reopened
slip can show the `order_failed` banner) and on provider unmount (so we
don't `setState` after teardown).
- **Tap Retry** → cancels the auto-clear timer and reopens the slip with
the same market context. The reopened slip's existing inline
`order_failed` banner handles the per-slip error UX (preserves PR
#29184's intent).
#### Toast shape
- Variant: `ToastVariants.Icon`
- Layout: `[avatar icon] [bold label + description] [Retry]` on a single
row.
- `iconName`: `IconName.Error`
- `iconColor`: `theme.colors.error.default` (red — _see "known
limitations" below_)
- `backgroundColor`: `theme.colors.error.muted` (soft red wash, matching
the standard error-avatar look used in `NetworkConnectionBanner`,
`ErrorBoundary`, `AlertModal`, etc.)
- `hasNoTimeout: false` (auto-dismisses on platform default ~2.75s
visibility + 0.25s exit)
- `closeButtonOptions`: `{ label: 'Retry', variant: ButtonVariants.Link,
onPress }` — the inline Retry action
#### Locale keys
All existing — no new strings:
- `predict.order.prediction_failed` — toast title
- `predict.order.order_failed_generic` — toast description
- `predict.order.retry` — Retry action label
### Out of scope (intentionally)
- The shared `Toast` component is **unchanged** on this PR (an earlier
draft added an opt-in `compact` prop, which has since been reverted in
favor of the existing `closeButtonOptions` API).
- The deposit / withdraw / claim error toasts in
`usePredictToastRegistrations.tsx` continue to use the existing
`accent04.normal` indigo background — only the new bottom-sheet failure
toast was switched to the conventional `error.muted` red wash.
Harmonizing the rest is a follow-up.
### Known limitations
- The `error.svg` asset
(`app/component-library/components/Icons/Icon/assets/error.svg`) has
hardcoded `fill="none"` on the root and `fill="#121314"` on the path, so
the small Error glyph paints near-black regardless of the `iconColor` we
pass. This affects every `IconName.Error` callsite in the app, not just
ours. Filed for the design-system-engineers team. The `error.muted` soft
red background masks the issue here visually (dark glyph on light red
wash reads correctly as "error"), but the glyph itself only becomes red
once the SVG asset is fixed upstream.
## **Changelog**
CHANGELOG entry: Fixed an issue where the Predict bet slip would
unexpectedly reopen during a pay-with-any-token deposit and remain open
after the order completed. Background failures now surface a "Failed to
place prediction" toast with a Retry action that reopens the slip with
the order-failed banner; if the user doesn't tap Retry, the toast
auto-dismisses and the order error is cleared automatically.
## **Related issues**
Fixes:
Jira Ticket: https://consensyssoftware.atlassian.net/browse/PRED-883
## **Manual testing steps**
```gherkin
Feature: Predict bet slip stays closed during PWAT deposit; failures show a Retry toast
Background:
Given the user has the predictBottomSheet feature flag enabled
And the user is on a Predict market
Scenario: Successful PWAT bet does not reopen the slip
Given the user has chosen an external token (e.g. ETH) as the payment method
When the user enters an amount and taps Confirm
Then the bet slip closes via animation
And the "Prediction in progress" toast appears
When the deposit and order confirm on-chain
Then the loading toast is replaced by the "Prediction placed" success toast
And the bet slip does NOT reopen at any point during the flow
Scenario: Background failure surfaces a Retry toast that reopens the slip
Given the user has confirmed a PWAT bet and the slip has closed
When the order fails in the background
Then a "Failed to place prediction" toast appears on a soft red avatar background
And the toast shows a "Transaction failed. Please try again." description
And the toast shows a "Retry" link inline on the right
When the user taps Retry within the toast's visibility window
Then the bet slip reopens at the same market with the inline "Order failed" banner and a Retry CTA
Scenario: User ignores the failure toast — error auto-clears
Given the failure toast is visible
When the user takes no action for ~3 seconds
Then the toast auto-dismisses
And the active order error is cleared automatically
When the user opens any market's bet slip
Then no inline order_failed banner is shown (clean state)
Scenario: Failure while the slip is currently visible
Given the bet slip is currently open (e.g. the user reopened it manually mid-flight)
When the order fails
Then no toast appears (the inline banner inside the slip handles it)
Scenario: Bottom-sheet flow disabled — legacy failure toast still works
Given the predictBottomSheet feature flag is OFF
And the user has confirmed a bet that fails in the background
Then the legacy "order failed" toast from usePredictToastRegistrations fires
And the bottom-sheet provider's toast does NOT fire
```
## **Screenshots/Recordings**
### **Before**
<!-- Drop a recording of the bet slip popping back open over the loading
toast during a PWAT deposit, and staying stuck open after the order
completed. -->
### **After**
<!-- Drop a recording of:
1. Successful PWAT bet (slip closes, loading toast, success toast — no
reopen)
2. Failure path showing the auto-dismissing soft-red Retry toast
3. Tapping Retry reopens the slip with the inline order_failed banner
4. Letting the toast time out (no Retry tap) — next slip open is a clean
state
-->
https://github.com/user-attachments/assets/968fc06c-b937-4fc4-a5ca-e9d999b05278
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable —
`PredictPreviewSheetContext.test.tsx` (28 tests, including a dedicated
`failure toast (state-based trigger)` suite and a `failure toast
auto-clear timer` suite using `jest.useFakeTimers()`) and updated
`usePredictToastRegistrations.test.tsx` for the suppression branch.
Coverage on touched files: rerun `yarn jest --coverage` after final
cleanup and update.
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable — provider helpers, the `clearErrorTimerRef` rationale,
and the state-based trigger comment block.
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
#### Performance checks (if applicable)
- [ ] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics — N/A, no new long-running operations introduced.
## **Pre-merge reviewer checklist**
- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes Predict order-failure UX from auto-reopening the bottom sheet
to a state-driven toast with retry and an auto-clear timer, which could
affect error handling timing and user flows. Also adjusts toast
suppression logic to avoid duplicates when the provider is mounted.
>
> **Overview**
> Predict bottom-sheet order failures no longer auto-reopen the buy
slip; instead `PredictPreviewSheetProvider` watches `activeOrder.error`
transitions and shows a non-persistent **Retry** toast (via
`ToastService`) that reopens the slip with the last buy params only if
the user taps it.
>
> Adds a ~3s auto-clear timer to call `clearOrderError()` after the
toast dismisses (cancelled on Retry and on provider unmount) to avoid
stale inline error banners, and updates `usePredictToastRegistrations`
to suppress its legacy `'failed'` toast when the provider is mounted.
>
> Tests were expanded/updated to cover the new toast trigger conditions,
retry behavior, timer cancellation/cleanup, and to harden hook tests
against leaked mounts/promises.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
adcbad0. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Caainã Jeronimo <caainaje@gmail.com>1 parent b2e95cf commit 94e8c0f
5 files changed
Lines changed: 320 additions & 66 deletions
File tree
- app/components/UI/Predict
- contexts
- hooks
Lines changed: 173 additions & 22 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
62 | 63 | | |
63 | 64 | | |
64 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
65 | 88 | | |
66 | 89 | | |
67 | 90 | | |
| |||
204 | 227 | | |
205 | 228 | | |
206 | 229 | | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
207 | 237 | | |
208 | 238 | | |
209 | 239 | | |
| |||
442 | 472 | | |
443 | 473 | | |
444 | 474 | | |
445 | | - | |
446 | | - | |
| 475 | + | |
| 476 | + | |
447 | 477 | | |
448 | 478 | | |
449 | 479 | | |
| |||
465 | 495 | | |
466 | 496 | | |
467 | 497 | | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
468 | 530 | | |
| 531 | + | |
469 | 532 | | |
470 | 533 | | |
471 | | - | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
472 | 555 | | |
473 | 556 | | |
474 | 557 | | |
475 | 558 | | |
476 | 559 | | |
477 | 560 | | |
478 | 561 | | |
479 | | - | |
480 | 562 | | |
481 | 563 | | |
482 | 564 | | |
| |||
486 | 568 | | |
487 | 569 | | |
488 | 570 | | |
489 | | - | |
490 | | - | |
491 | | - | |
| 571 | + | |
492 | 572 | | |
493 | 573 | | |
494 | | - | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
495 | 581 | | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
496 | 587 | | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
497 | 592 | | |
498 | 593 | | |
499 | 594 | | |
500 | 595 | | |
501 | 596 | | |
502 | 597 | | |
503 | 598 | | |
504 | | - | |
505 | | - | |
506 | 599 | | |
507 | | - | |
508 | | - | |
509 | | - | |
510 | 600 | | |
| 601 | + | |
511 | 602 | | |
512 | 603 | | |
513 | 604 | | |
514 | 605 | | |
515 | 606 | | |
| 607 | + | |
516 | 608 | | |
517 | | - | |
518 | | - | |
519 | | - | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
520 | 616 | | |
| 617 | + | |
521 | 618 | | |
522 | | - | |
523 | | - | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
524 | 630 | | |
525 | 631 | | |
526 | 632 | | |
527 | 633 | | |
528 | 634 | | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
529 | 642 | | |
530 | 643 | | |
531 | 644 | | |
532 | 645 | | |
533 | 646 | | |
534 | 647 | | |
535 | 648 | | |
536 | | - | |
537 | | - | |
538 | | - | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
539 | 690 | | |
540 | 691 | | |
541 | 692 | | |
| |||
0 commit comments