chore(runway): cherry-pick feat(predict): Bottom Sheet - Try Again Toast for failed Payments cp-7.77.0#30206
Merged
Conversation
…ast 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>
Contributor
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
Contributor
🔍 Smart E2E Test Selection⏭️ Smart E2E selection skipped - PR targets a release branch (release/*) All E2E tests pre-selected. |
|
Dismissing my own approval, discussion ongoing.
vpintorico
approved these changes
May 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



PR: fix(predict): replace bet slip auto-reopen with auto-dismissing
Retry toast
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
useEffectinPredictPreviewSheetContext— added in #29184 to surface inline error banners after background
failures — fired on any transient
activeOrder.errorvalue. ThePredictController briefly sets
errorduring its internal retry paths(
PredictController.ts:1277and:2300) even on flows that ultimatelysucceed, so the slip popped back up over toasts that were still
mid-loading. The reopened slip didn't close on
SUCCESSeither, becausethe freshly-mounted
usePredictBuyActionsinstance hasdidInitiateOrderRef = falseand 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_failedbanner. If the user does nothing, the toastfades out and
activeOrder.erroris automatically cleared so the nextslip open is a clean state (no stale banner flash).
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 endKey changes
useEffectanddismissedWithErrorReffrom
PredictPreviewSheetContext.tsx.via
ToastService.showToast(...)wheneveractiveOrder.errortransitions 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 onisBackgroundOrderthat theevent-based path is subject to.
isPredictSheetProviderMounted()so the legacyevent-based toast in
usePredictToastRegistrations.tsxcan suppress itself when the provider is mounted (avoids a duplicate
failure toast).
clearErrorTimerRef3-second timer that callsclearOrderError()after the toast auto-dismisses, so an unhandledfailure doesn't leave a stale
activeOrder.errorfor the next slipopen. The timer is cancelled when the user taps Retry (so the reopened
slip can show the
order_failedbanner) and on provider unmount (so wedon't
setStateafter teardown).the same market context. The reopened slip's existing inline
order_failedbanner handles the per-slip error UX (preserves PRfeat(predict): bPredict Bottom Sheet Errors PRED-836 #29184's intent).
Toast shape
ToastVariants.Icon[avatar icon] [bold label + description] [Retry]on a singlerow.
iconName:IconName.ErroriconColor:theme.colors.error.default(red — see "knownlimitations" below)
backgroundColor:theme.colors.error.muted(soft red wash, matchingthe standard error-avatar look used in
NetworkConnectionBanner,ErrorBoundary,AlertModal, etc.)hasNoTimeout: false(auto-dismisses on platform default ~2.75svisibility + 0.25s exit)
closeButtonOptions:{ label: 'Retry', variant: ButtonVariants.Link, onPress }— the inline Retry actionLocale keys
All existing — no new strings:
predict.order.prediction_failed— toast titlepredict.order.order_failed_generic— toast descriptionpredict.order.retry— Retry action labelOut of scope (intentionally)
Toastcomponent is unchanged on this PR (an earlierdraft added an opt-in
compactprop, which has since been reverted infavor of the existing
closeButtonOptionsAPI).usePredictToastRegistrations.tsxcontinue to use the existingaccent04.normalindigo background — only the new bottom-sheet failuretoast was switched to the conventional
error.mutedred wash.Harmonizing the rest is a follow-up.
Known limitations
error.svgasset(
app/component-library/components/Icons/Icon/assets/error.svg) hashardcoded
fill="none"on the root andfill="#121314"on the path, sothe small Error glyph paints near-black regardless of the
iconColorwepass. This affects every
IconName.Errorcallsite in the app, not justours. Filed for the design-system-engineers team. The
error.mutedsoftred 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
Screenshots/Recordings
Before
After
newErrorToastV2.mov
Pre-merge author checklist
Docs and MetaMask Mobile
Coding
Standards.
PredictPreviewSheetContext.test.tsx(28 tests, including a dedicatedfailure toast (state-based trigger)suite and afailure toast auto-clear timersuite usingjest.useFakeTimers()) and updatedusePredictToastRegistrations.test.tsxfor the suppression branch.Coverage on touched files: rerun
yarn jest --coverageafter finalcleanup and update.
if applicable — provider helpers, the
clearErrorTimerRefrationale,and the state-based trigger comment block.
guidelines).
Not required for external contributors.
Performance checks (if applicable)
SRPs
to import wallets with many accounts and tokens
performance metrics — N/A, no new long-running operations introduced.
Pre-merge reviewer checklist
app, test code being changed).
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
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
PredictPreviewSheetProviderwatchesactiveOrder.errortransitions and shows a non-persistent Retry toast (via
ToastService) that reopens the slip with the last buy params only ifthe user taps it.
Adds a ~3s auto-clear timer to call
clearOrderError()after thetoast dismisses (cancelled on Retry and on provider unmount) to avoid
stale inline error banners, and updates
usePredictToastRegistrationsto 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.
Reviewed by Cursor Bugbot for commit
adcbad0. Bugbot is set up for automated
code reviews on this repo. Configure
here.
Co-authored-by: Caainã Jeronimo caainaje@gmail.com 94e8c0f