Skip to content

feat(predict): Bottom Sheet - Try Again Toast for failed Payments cp-7.77.0#30167

Merged
matallui merged 18 commits into
mainfrom
fix-bottomsheet-pop
May 14, 2026
Merged

feat(predict): Bottom Sheet - Try Again Toast for failed Payments cp-7.77.0#30167
matallui merged 18 commits into
mainfrom
fix-bottomsheet-pop

Conversation

@MarioAslau

@MarioAslau MarioAslau commented May 14, 2026

Copy link
Copy Markdown
Contributor

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 — 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).

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
Loading

Key changes

  • Removed the auto-reopen useEffect and dismissedWithErrorRef from 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 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 feat(predict): bPredict Bottom Sheet Errors PRED-836 #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

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

After

newErrorToastV2.mov

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs and MetaMask Mobile Coding Standards.
  • I've completed the PR template to the best of my ability
  • 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.
  • I've documented my code using JSDoc format if applicable — provider helpers, the clearErrorTimerRef rationale, and the state-based trigger comment block.
  • I've applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs 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

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • 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.

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.

Reviewed by Cursor Bugbot for commit adcbad0. Bugbot is set up for automated code reviews on this repo. Configure here.

@MarioAslau MarioAslau self-assigned this May 14, 2026
@github-actions

Copy link
Copy Markdown
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.

@MarioAslau MarioAslau added the team-predict Predict team label May 14, 2026
@metamaskbotv2 metamaskbotv2 Bot added the team-mobile-platform Mobile Platform team label May 14, 2026
@MarioAslau MarioAslau marked this pull request as ready for review May 14, 2026 02:50
@MarioAslau MarioAslau requested review from a team as code owners May 14, 2026 02:50
@github-actions github-actions Bot added size-M and removed size-L labels May 14, 2026

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 21e91db. Configure here.

Comment thread app/components/UI/Predict/contexts/PredictPreviewSheetContext.tsx Outdated
caieu
caieu previously approved these changes May 14, 2026

@caieu caieu 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.

LGTM

@MarioAslau MarioAslau changed the title feat(predict): Bottom Sheet - Try Again Toast for failed Payments feat(predict): Bottom Sheet - Try Again Toast for failed Payments cp-7.77.0 May 14, 2026
@matallui matallui enabled auto-merge May 14, 2026 15:05
Comment on lines +295 to +302
ToastService.showToast({
variant: ToastVariants.Icon,
labelOptions: [
{
label: strings('predict.order.prediction_failed'),
isBold: true,
},
],

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.

Why call showToast directly here, rather than catching the failure as a PredictController event and trigger the toast in usePredictToastRegistrations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I started with that approach (you can see the legacy fallback for it still in. I moved to a state-based trigger here because of a timing race in the event path:

The controller publishes 'failed' only when isBackgroundOrder is true
eg: when state.activeBuyOrders[address].transactionId differs from params.transactionId. That field is cleared by the slip's unmount cleanup (clearActiveOrderTransactionId). On the happy path the slip animates closed → cleanup runs → transactionId is undefined → eventually deposit confirms → isBackgroundOrder is true → event publishes.

But on faster networks (or when the user reopens the slip mid-flight) the deposit can confirm before the cleanup runs, so the state still has transactionId set → isBackgroundOrder is false → no 'failed' event → no toast. I hit this on device while testing PWAT with ETH, the failure was completely silent.

A state-based trigger sidesteps this entirely: we react to activeOrder.error transitioning falsy → truthy, which is a reliable React signal regardless of timing. The provider already subscribes to usePredictActiveOrder() for other reasons, so reading error here is essentially free.

The event-based handler is still wired up as a fallback (suppressed when the provider is mounted to avoid double-firing), so the legacy non-bottom-sheet flow keeps working unchanged.

@brianacnguyen brianacnguyen 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.

Toast change is not needed

caieu
caieu previously approved these changes May 14, 2026

@caieu caieu 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.

LGTM

@matallui matallui removed the request for review from chaoticgoodpanda May 14, 2026 17:49
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePredictions, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 90%
click to see 🤖 AI reasoning details

E2E Test Selection:
The changes are scoped entirely to the Predict feature (Polymarket prediction market integration). The key changes are:

  1. PredictPreviewSheetContext.tsx: Refactored order failure handling - replaced auto-reopen behavior with a "Try again" toast. Added timer-based auto-clearing of order errors (~3s after toast auto-dismisses), theme integration for toast colors, and improved state transition tracking using previousErrorRef to only fire on falsy→truthy transitions.

  2. usePredictToastRegistrations.tsx: Minor comment update clarifying the suppression logic when the provider is mounted.

  3. Test files: Updated unit tests to reflect new toast-based failure flow, added timer tests with fake timers, and fixed test cleanup issues in usePredictMarketData.

Tag selection rationale:

  • SmokePredictions: Directly impacted - the order failure UX flow changed (toast instead of auto-reopen). E2E tests covering position lifecycle, error handling scenarios, and the full prediction flow need to validate this new behavior.
  • SmokeWalletPlatform: Required per SmokePredictions tag description - Predictions is a section inside the Trending tab, so changes to Predictions views affect Trending.
  • SmokeConfirmations: Required per SmokePredictions tag description - opening/closing positions are on-chain transactions that go through the confirmations flow.

No other areas are affected - no changes to core infrastructure, navigation, shared components, or other feature areas.

Performance Test Selection:
The changes are purely behavioral/UX changes to the order failure flow in the Predict feature - replacing auto-reopen with a toast notification. There are no changes to rendering performance, data loading, list components, or app initialization that would warrant performance testing. The timer-based error clearing is a lightweight operation that won't impact measurable performance metrics.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@MarioAslau MarioAslau dismissed brianacnguyen’s stale review May 14, 2026 18:17

no longer making any DS changes

@matallui matallui added this pull request to the merge queue May 14, 2026
Merged via the queue into main with commit 94e8c0f May 14, 2026
183 of 185 checks passed
@matallui matallui deleted the fix-bottomsheet-pop branch May 14, 2026 19:42
@github-actions github-actions Bot locked and limited conversation to collaborators May 14, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.78.0 Issue or pull request that will be included in release 7.78.0 label May 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.78.0 Issue or pull request that will be included in release 7.78.0 size-M team-mobile-platform Mobile Platform team team-predict Predict team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants