Skip to content

chore(runway): cherry-pick feat(predict): Implement Predict Bet Slip PRED-707 cp-7.74.0#28955

Merged
chloeYue merged 1 commit into
release/7.74.00from
runway-cherry-pick-7.74.0-1776378053
Apr 17, 2026
Merged

chore(runway): cherry-pick feat(predict): Implement Predict Bet Slip PRED-707 cp-7.74.0#28955
chloeYue merged 1 commit into
release/7.74.00from
runway-cherry-pick-7.74.0-1776378053

Conversation

@runway-github

@runway-github runway-github Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Description

Migrate the Predict Buy and Sell preview screens from full-screen stack
navigation into BottomSheet wrappers, gated behind the
predictBottomSheet LaunchDarkly feature flag. When the flag is OFF,
the existing full-screen navigation flow is preserved with zero
behavioral changes.

What changed

New: PredictPreviewSheet wrapper component

  • Generic BottomSheet wrapper that renders buy/sell preview content as
    children.
  • Uses BottomSheet, BottomSheetHeader, and
    BottomSheetHeaderVariant from @metamask/design-system-react-native
    (not the deprecated component-library version).
  • Supports renderHeader prop for custom header content (sell sheet
    uses it for rich cashout info); falls back to default
    icon+title+subtitle layout (buy sheet).
  • isFullscreen prop controls sheet height: content-fitted for both buy
    and sell sheets (auto-sizes to content).
  • Title and subtitle elements expose testID props
    (preview-sheet-title, preview-sheet-subtitle) for testID-based
    assertions.
  • Unit tests for the wrapper component.

New: PredictPreviewSheetContext (central sheet state manager)

  • Context provider manages both buy and sell sheet refs, params, and
    open/close lifecycle.
  • Exposes openBuySheet() / openSellSheet() methods consumed by all
    card components.
  • Feature flag check determines whether to navigate (old flow) or open a
    sheet (new flow).
  • SellSheetHeader extracted as a standalone component within the
    context for the rich header.
  • Graceful fallback: usePredictPreviewSheet() returns navigation-based
    routing when used outside the provider (e.g. home carousel, trending
    feed) instead of throwing, with memoized references to prevent
    unnecessary re-renders.
  • Nonce-based re-open mechanism to handle identical params across
    consecutive sheet opens.
  • Unit tests for the context provider.

New: predictBottomSheet feature flag

  • Registered in feature-flag-registry.ts as a remote, version-gated
    flag (default: disabled).
  • New selector selectPredictBottomSheetEnabledFlag in
    selectors/featureFlags/index.ts.

New: PredictQuickAmounts component (with haptic feedback)

  • Standalone quick-pick buttons ($20, $50, $100, $250) displayed in the
    bottom content area of the buy sheet.
  • Uses expo-haptics (impactAsync with ImpactFeedbackStyle.Light)
    for tactile feedback on button press. impactAsync is properly awaited
    with error swallowing for unsupported devices.
  • Quick amounts set the input to whole numbers (e.g. "20" not
    "20.00") for easier editing.
  • Disabled buttons are verified non-functional in tests.
  • Uses design-system Box component instead of raw View.
  • Unit tests including haptic feedback verification and async handler
    coverage.

New: usePredictCashOut hook (shared cashout logic)

  • Encapsulates the guarded cashout action flow: find outcome by
    position.outcomeId, call openSellSheet, and handle errors with
    Logger.error + toast notification.
  • Accepts market and callerName parameters; callerName is included
    in error metadata for production diagnostics.
  • Consumed by both PredictPicks and PredictPositionDetail,
    eliminating ~40 lines of identical code from each.
  • Exported from hooks/index.ts.
  • Unit tests covering guarded action invocation, openSellSheet params,
    error/toast on missing outcome, and callerName in metadata.

Navigation migration (10 components updated)
All components that previously called navigateToBuyPreview() /
navigate(Routes.PREDICT.MODALS.SELL_PREVIEW) now call openBuySheet()
/ openSellSheet() from usePredictPreviewSheet:

  • PredictMarketDetails - outcome buy actions
  • PredictMarketSingle - Yes/No buy buttons
  • PredictMarketMultiple - outcome buy buttons
  • PredictMarketOutcome - price buttons
  • PredictSportCardFooter - feed card buy actions
  • FeaturedCarouselCard - carousel card outcome buy buttons
  • FeaturedCarouselSportCard - sport carousel card buy buttons
  • PredictPicks - cashout from picks list via shared
    usePredictCashOut hook
  • PredictPositionDetail - cashout from position detail via shared
    usePredictCashOut hook
  • Removed direct dependency on usePredictNavigation from all these
    components.

Routes (routes/index.tsx)

  • Wrapped the stack navigator with PredictPreviewSheetProvider so
    sheets are available throughout the Predict navigation tree.

Buy sheet (PredictBuyWithAnyToken)

  • Uses discriminated union props (mode: 'sheet' | 'screen') for
    type-safe sheet vs navigation mode, replacing Partial<ContentProps>
    with as casts.
  • Sheet mode layout: header hidden (provided by PredictPreviewSheet),
    PredictQuickAmounts in bottom content, PredictPayWithRow in compact
    variant="row" mode with balance display, keypad moved below bottom
    content with hideHeader, border hidden, action button displays
    "Confirm".
  • Uses Box wrapper instead of SafeAreaView in sheet mode.
  • PredictPayWithAnyTokenInfo receives isInputFocused: false in sheet
    mode to ensure mm_pay relay configuration runs immediately (prevents
    underfunded deposit).
  • Sheet cleanup: unmount runs onReject +
    clearActiveOrderTransactionId (matches beforeRemove behavior in old
    flow).
  • Approval request recovery: when approvalRequest is missing during
    handleConfirm in PAY_WITH_ANY_TOKEN state, attempts re-initialization
    via initPayWithAnyToken() (clears batchIdRef, rejects pending
    transactions, re-creates the approval) before returning
    PLACE_ORDER_FAILED. This gives the user a chance to retry with a fresh
    approval rather than silently failing.

Buy sheet (PredictBuyPreview - legacy non-pay-with-any-token)

  • Same discriminated union props / isSheetMode pattern for BottomSheet
    compatibility.
  • Header and back button hidden in sheet mode; close action routed to
    onClose().
  • Uses ScrollView from react-native-gesture-handler (aliased as
    GHScrollView) in sheet mode to cooperate with the BottomSheet's
    PanGestureHandler; standard React Native ScrollView preserved for
    the full-screen path.

Sell sheet (PredictSellPreview)

  • Same discriminated union props pattern; isSheetMode drives
    conditional layout: icon+title row hidden in sheet mode (rendered by
    SellSheetHeader in the sheet header instead), price/shares/PnL section
    relocated to the bottom area.
  • Sheet mode uses inline Tailwind tw.style('flex-col') instead of
    StyleSheet.create() for the container.
  • Extracted getCashoutInfoText helper into utils/format.ts to DRY up
    the localized cashout info string.

PredictController (batch transaction fix)

  • Added gasFeeToken: MATIC_CONTRACTS.collateral to the
    initPayWithAnyToken batch submission, matching other batch calls. This
    ensures mm_pay configures the relay step to bridge funds when the Safe
    has insufficient USDC balance.

PredictKeypad

  • New hideHeader prop to suppress the quick-amount row + Done button
    when rendered inside the buy sheet (avoids duplication with
    PredictQuickAmounts).

PredictPayWithRow

  • New variant="row" mode: compact single-row layout showing token
    icon, symbol, balance, and right chevron for the sheet.
  • New availableBalance prop for displaying balance in row mode.

PredictBuyAmountSection

  • New hideAvailableBalance prop to suppress the "Available: $X.XX"
    text in sheet mode.

PredictBuyBottomContent

  • New hideBorder prop to remove the top border separator in sheet
    mode.
  • Restored inline text wrapping for the disclaimer + "Learn more" link
    (nested Text instead of sibling flex items).

PredictBuyActionButton

  • New isSheetMode prop: when true, displays "Confirm" label instead of
    "{outcome} · {price}".

PredictFeeSummary

  • Removed "incl. fees" subtitle text; moved info icon inline next to
    "Total" on the same line.
  • Total amount typography changed from HeadingMd/Bold to
    BodyMd/Medium to match Pay With row.

Bug fixes

  • usePredictBuyConditions: Auto-revert effect that switched back to
    Predict balance now requires totalPayForPredictBalance > 0, preventing
    immediate revert when the amount is 0 (which always caused the payment
    method selection to snap back in sheet mode).
  • usePredictBuyConditions: shouldWaitForPayFees now also requires
    currentValue > 0.
  • usePredictBuyActions: When approvalRequest is missing during
    handleConfirm in PAY_WITH_ANY_TOKEN state, attempts re-initialization
    (initPayWithAnyToken()) before returning early with error, giving the
    user a retry path rather than silently failing. Removed dead fallback
    branch that was unreachable (transactionId derived from the same
    approvalRequest).
  • usePredictBuyActions: batchIdRef cleared at the start of
    doInit() to prevent stale values between transaction attempts.
  • usePredictBuyActions: Sheet unmount cleanup mirrors beforeRemove
    behavior from the old flow.
  • usePredictBuyActions: DEPOSITING and SUCCESS state effects check
    isSheetMode to call onClose() instead of StackActions.pop().
  • usePredictBuyError: Removed isInputFocused from the
    blockingPayAlertMessage gate. The flag was permanently suppressing
    insufficient-funds errors in sheet mode because isInputFocused
    initialized to true and never transitioned to false when using
    quick-amount buttons. The isPayFeesLoading check already handles
    stale-data concerns.
  • PredictPicks / PredictPositionDetail: Cashout logic (outcome
    lookup, openSellSheet, error handling with Logger.error + toast)
    extracted into shared usePredictCashOut hook to eliminate duplication.
  • PredictPayWithAnyTokenInfo: isInputFocused overridden to false
    in sheet mode so updatePendingAmount and setPayToken run
    immediately, ensuring mm_pay relay step is configured before
    confirmation.
  • PredictController: Added gasFeeToken to initPayWithAnyToken
    batch to fix missing mm_pay relay step that caused "Insufficient USDC
    balance in Safe" errors.
  • PredictPreviewSheetContext: Stabilized onDismiss callbacks with
    useCallback to prevent unstable closeSheet/onClose references from
    causing repeated SUCCESS/DEPOSITING effect re-fires.

Code quality

  • Extracted shared usePredictCashOut hook to deduplicate identical
    onCashOut logic (~40 lines each) from PredictPicks and
    PredictPositionDetail. Both components now use a single hook call
    instead of duplicating the guarded action, outcome lookup,
    openSellSheet call, and error handling with Logger.error + toast.
    The callerName parameter preserves per-component error metadata.
  • Replaced raw View with design-system Box in PredictQuickAmounts.
  • Removed stale comments from usePredictBottomSheet.
  • Discriminated union types (PredictBuyPreviewProps,
    PredictSellPreviewProps) replace Partial<ContentProps> with as
    casts, providing compile-time safety for sheet vs navigation mode.
  • New test IDs: BUY_PREVIEW_SHEET, SELL_PREVIEW_SHEET.
  • Memoized fallback return value in usePredictPreviewSheet to prevent
    unstable function references.
  • Memoized BuyComponent selection in PredictPreviewSheetContext with
    useMemo to prevent unnecessary unmount/remount cycles.
  • Added index.ts barrel export for PredictPreviewSheet component
    (file organization standard).
  • Restored slide-from-right cardStyleInterpolator on BUY_PREVIEW and
    SELL_PREVIEW stack screens for the flag-OFF full-screen path.
  • Replaced require() with jest.requireActual() in all test mock
    factories.
  • Fixed module-level shared mutable state (navigationRef) in
    routes/index.test.tsx — moved to beforeEach.
  • Strengthened test assertions in PredictMarketDetails.test.tsx to
    verify exact call parameters with expect.objectContaining().
  • Removed dead/unused predict.order.buy i18n key and its stale test
    mock reference.
  • Migrated PredictPreviewSheet from deprecated component-library
    BottomSheet to @metamask/design-system-react-native
    BottomSheet/BottomSheetHeader (DS-first UI rule). Updated
    usePredictBottomSheet hook to import BottomSheetRef from DS.
  • Replaced shouldNavigateBack={false} with DS equivalent (omit
    goBack prop); changed header style prop to twClassName.
  • PredictBuyPreview: Uses gesture-handler-aware ScrollView from
    react-native-gesture-handler in sheet mode to prevent scroll/dismiss
    conflicts with the BottomSheet's PanGestureHandler on Android.
  • Replaced all toBeNull() assertions with not.toBeOnTheScreen() in
    test files per unit-test guidelines.
  • Replaced text-based queries (getByText, queryByText) with
    testID-based assertions (getByTestId + toHaveTextContent) in
    PredictPreviewSheet.test.tsx and
    PredictPreviewSheetContext.test.tsx.
  • Changed synchronous act() to await act(async () => { ... }) in
    routes/index.test.tsx to flush async navigator state updates and
    eliminate act() warnings.

Localization

  • Added predict.odds key ("Odds").
  • Added predict.order.confirm key ("Confirm") for the sheet action
    button.
  • Removed unused predict.order.buy key ("Buy {{outcome}}").

Documentation

  • Added comprehensive Predictions architecture guide
    (docs/predict/predictions-comprehensive-guide.md).
  • Added ticket documentation
    (docs/predict/tickets/buy-sell-bottomsheet-migration.md).

Changelog

CHANGELOG entry: null

Related issues

Refs:
PRED-707

Manual testing steps

Feature: Predict Buy/Sell BottomSheet Migration

  Scenario: Buy prediction via bottom sheet (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the user is on a prediction market details page

    When user taps a "Yes" or "No" outcome button
    Then a bottom sheet opens with the buy preview content
    And the sheet header shows the outcome icon, "Yes/No · Outcome Name" title, and odds subtitle
    And quick amount buttons ($20, $50, $100, $250) are visible with haptic feedback
    And the user can enter an amount, select payment method, and tap "Confirm"
    And the order is placed successfully
    And the sheet closes after the deposit is initiated

  Scenario: Sell position via bottom sheet (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the user has an active position on a market

    When user taps "Cash out" on their position
    Then a content-fitted bottom sheet opens (not fullscreen)
    And the sheet header shows the position icon, title, and cashout info
    And the body shows the current value, shares, PnL, and Cash out button
    And tapping "Cash out" places the sell order and closes the sheet

  Scenario: Buy prediction via full-screen navigation (flag OFF)
    Given the predictBottomSheet feature flag is disabled
    And the user is on a prediction market details page

    When user taps a "Yes" or "No" outcome button
    Then the app navigates to the full-screen BuyPreview screen
    And the existing layout and behavior are unchanged

  Scenario: Sell position via full-screen navigation (flag OFF)
    Given the predictBottomSheet feature flag is disabled
    And the user has an active position on a market

    When user taps "Cash out" on their position
    Then the app navigates to the full-screen SellPreview screen
    And the existing layout and behavior are unchanged

  Scenario: Change payment method in buy sheet (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the buy bottom sheet is open

    When user taps the Pay with row and selects a different token (e.g. USDC)
    Then the payment method updates and stays selected
    And the fee summary and total update accordingly

  Scenario: Buy from feed cards and carousel (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the user is on the Predict feed or homepage carousel

    When user taps a Yes/No button on any market card
    Then the bottom sheet opens with the correct market/outcome pre-filled
    And the flow works identically to opening from market details

  Scenario: Components outside PredictScreenStack (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And a Predict card is rendered on the homepage carousel (outside PredictScreenStack)

    When user taps a Yes/No button
    Then the app navigates via React Navigation (fallback behavior)
    And no crash occurs from missing PredictPreviewSheetProvider

  Scenario: Quick amount haptic feedback (flag ON)
    Given the buy bottom sheet is open

    When user taps a quick amount button ($20, $50, $100, or $250)
    Then haptic feedback fires (Light impact)
    And the amount input updates to the selected value
    And the input unfocuses

Screenshots/Recordings

Before

Full-screen buy/sell preview navigation (flag OFF) - no visual changes
to this flow.

After

newDemoPRedict.mov

Pre-merge author checklist

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
Touches Predict trade entry points and preview/cash-out flows, plus
introduces a new feature-flagged UI layer; regressions could affect
order preview UX or sheet/navigation behavior, though the legacy route
path remains as fallback.

Overview
Adds a new feature-flagged bet slip flow for Predict by introducing
PredictPreviewSheet +
PredictPreviewSheetProvider/usePredictPreviewSheet, allowing
buy/sell previews to open in BottomSheets (with navigation fallback when
the provider/flag isn’t available).

Updates multiple Predict entry points (feed cards, carousel cards,
market single/multiple/outcome components, and sport footer) to call
openBuySheet instead of navigating to the buy preview, and refactors
cash-out handling into a shared usePredictCashOut hook that opens the
sell sheet and handles missing-outcome errors via logging + toast.

Extends buy preview screens (PredictBuyPreview,
PredictBuyWithAnyToken) to support a new mode: 'sheet' rendering
path (layout tweaks, keypad/header behavior, confirm CTA), adds new test
IDs/feature-flag selectors (predictBottomSheet), and adjusts provider
order result shape and assorted tests to match the new behavior.

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


Co-authored-by: Caainã Jeronimo caainaje@gmail.com
Co-authored-by: Luis Taniça matallui@gmail.com 27617bd

…PRED-707 cp-7.74.0 (#28779)

## **Description**

Migrate the Predict Buy and Sell preview screens from full-screen stack
navigation into BottomSheet wrappers, gated behind the
`predictBottomSheet` LaunchDarkly feature flag. When the flag is OFF,
the existing full-screen navigation flow is preserved with zero
behavioral changes.

### What changed

**New: `PredictPreviewSheet` wrapper component**
- Generic BottomSheet wrapper that renders buy/sell preview content as
children.
- Uses `BottomSheet`, `BottomSheetHeader`, and
`BottomSheetHeaderVariant` from `@metamask/design-system-react-native`
(not the deprecated component-library version).
- Supports `renderHeader` prop for custom header content (sell sheet
uses it for rich cashout info); falls back to default
icon+title+subtitle layout (buy sheet).
- `isFullscreen` prop controls sheet height: content-fitted for both buy
and sell sheets (auto-sizes to content).
- Title and subtitle elements expose `testID` props
(`preview-sheet-title`, `preview-sheet-subtitle`) for testID-based
assertions.
- Unit tests for the wrapper component.

**New: `PredictPreviewSheetContext` (central sheet state manager)**
- Context provider manages both buy and sell sheet refs, params, and
open/close lifecycle.
- Exposes `openBuySheet()` / `openSellSheet()` methods consumed by all
card components.
- Feature flag check determines whether to navigate (old flow) or open a
sheet (new flow).
- `SellSheetHeader` extracted as a standalone component within the
context for the rich header.
- Graceful fallback: `usePredictPreviewSheet()` returns navigation-based
routing when used outside the provider (e.g. home carousel, trending
feed) instead of throwing, with memoized references to prevent
unnecessary re-renders.
- Nonce-based re-open mechanism to handle identical params across
consecutive sheet opens.
- Unit tests for the context provider.

**New: `predictBottomSheet` feature flag**
- Registered in `feature-flag-registry.ts` as a remote, version-gated
flag (default: disabled).
- New selector `selectPredictBottomSheetEnabledFlag` in
`selectors/featureFlags/index.ts`.

**New: `PredictQuickAmounts` component (with haptic feedback)**
- Standalone quick-pick buttons ($20, $50, $100, $250) displayed in the
bottom content area of the buy sheet.
- Uses `expo-haptics` (`impactAsync` with `ImpactFeedbackStyle.Light`)
for tactile feedback on button press. `impactAsync` is properly awaited
with error swallowing for unsupported devices.
- Quick amounts set the input to whole numbers (e.g. `"20"` not
`"20.00"`) for easier editing.
- Disabled buttons are verified non-functional in tests.
- Uses design-system `Box` component instead of raw `View`.
- Unit tests including haptic feedback verification and async handler
coverage.

**New: `usePredictCashOut` hook (shared cashout logic)**
- Encapsulates the guarded cashout action flow: find outcome by
`position.outcomeId`, call `openSellSheet`, and handle errors with
`Logger.error` + toast notification.
- Accepts `market` and `callerName` parameters; `callerName` is included
in error metadata for production diagnostics.
- Consumed by both `PredictPicks` and `PredictPositionDetail`,
eliminating ~40 lines of identical code from each.
- Exported from `hooks/index.ts`.
- Unit tests covering guarded action invocation, `openSellSheet` params,
error/toast on missing outcome, and `callerName` in metadata.

**Navigation migration (10 components updated)**
All components that previously called `navigateToBuyPreview()` /
`navigate(Routes.PREDICT.MODALS.SELL_PREVIEW)` now call `openBuySheet()`
/ `openSellSheet()` from `usePredictPreviewSheet`:
- `PredictMarketDetails` - outcome buy actions
- `PredictMarketSingle` - Yes/No buy buttons
- `PredictMarketMultiple` - outcome buy buttons
- `PredictMarketOutcome` - price buttons
- `PredictSportCardFooter` - feed card buy actions
- `FeaturedCarouselCard` - carousel card outcome buy buttons
- `FeaturedCarouselSportCard` - sport carousel card buy buttons
- `PredictPicks` - cashout from picks list via shared
`usePredictCashOut` hook
- `PredictPositionDetail` - cashout from position detail via shared
`usePredictCashOut` hook
- Removed direct dependency on `usePredictNavigation` from all these
components.

**Routes (`routes/index.tsx`)**
- Wrapped the stack navigator with `PredictPreviewSheetProvider` so
sheets are available throughout the Predict navigation tree.

**Buy sheet (`PredictBuyWithAnyToken`)**
- Uses discriminated union props (`mode: 'sheet' | 'screen'`) for
type-safe sheet vs navigation mode, replacing `Partial<ContentProps>`
with `as` casts.
- Sheet mode layout: header hidden (provided by `PredictPreviewSheet`),
`PredictQuickAmounts` in bottom content, `PredictPayWithRow` in compact
`variant="row"` mode with balance display, keypad moved below bottom
content with `hideHeader`, border hidden, action button displays
"Confirm".
- Uses `Box` wrapper instead of `SafeAreaView` in sheet mode.
- `PredictPayWithAnyTokenInfo` receives `isInputFocused: false` in sheet
mode to ensure mm_pay relay configuration runs immediately (prevents
underfunded deposit).
- Sheet cleanup: unmount runs `onReject` +
`clearActiveOrderTransactionId` (matches `beforeRemove` behavior in old
flow).
- Approval request recovery: when `approvalRequest` is missing during
`handleConfirm` in PAY_WITH_ANY_TOKEN state, attempts re-initialization
via `initPayWithAnyToken()` (clears `batchIdRef`, rejects pending
transactions, re-creates the approval) before returning
`PLACE_ORDER_FAILED`. This gives the user a chance to retry with a fresh
approval rather than silently failing.

**Buy sheet (`PredictBuyPreview` - legacy non-pay-with-any-token)**
- Same discriminated union props / `isSheetMode` pattern for BottomSheet
compatibility.
- Header and back button hidden in sheet mode; close action routed to
`onClose()`.
- Uses `ScrollView` from `react-native-gesture-handler` (aliased as
`GHScrollView`) in sheet mode to cooperate with the BottomSheet's
`PanGestureHandler`; standard React Native `ScrollView` preserved for
the full-screen path.

**Sell sheet (`PredictSellPreview`)**
- Same discriminated union props pattern; `isSheetMode` drives
conditional layout: icon+title row hidden in sheet mode (rendered by
`SellSheetHeader` in the sheet header instead), price/shares/PnL section
relocated to the bottom area.
- Sheet mode uses inline Tailwind `tw.style('flex-col')` instead of
`StyleSheet.create()` for the container.
- Extracted `getCashoutInfoText` helper into `utils/format.ts` to DRY up
the localized cashout info string.

**`PredictController` (batch transaction fix)**
- Added `gasFeeToken: MATIC_CONTRACTS.collateral` to the
`initPayWithAnyToken` batch submission, matching other batch calls. This
ensures mm_pay configures the relay step to bridge funds when the Safe
has insufficient USDC balance.

**`PredictKeypad`**
- New `hideHeader` prop to suppress the quick-amount row + Done button
when rendered inside the buy sheet (avoids duplication with
`PredictQuickAmounts`).

**`PredictPayWithRow`**
- New `variant="row"` mode: compact single-row layout showing token
icon, symbol, balance, and right chevron for the sheet.
- New `availableBalance` prop for displaying balance in row mode.

**`PredictBuyAmountSection`**
- New `hideAvailableBalance` prop to suppress the "Available: $X.XX"
text in sheet mode.

**`PredictBuyBottomContent`**
- New `hideBorder` prop to remove the top border separator in sheet
mode.
- Restored inline text wrapping for the disclaimer + "Learn more" link
(nested `Text` instead of sibling flex items).

**`PredictBuyActionButton`**
- New `isSheetMode` prop: when true, displays "Confirm" label instead of
"{outcome} · {price}".

**`PredictFeeSummary`**
- Removed "incl. fees" subtitle text; moved info icon inline next to
"Total" on the same line.
- Total amount typography changed from `HeadingMd`/`Bold` to
`BodyMd`/`Medium` to match Pay With row.

**Bug fixes**
- `usePredictBuyConditions`: Auto-revert effect that switched back to
Predict balance now requires `totalPayForPredictBalance > 0`, preventing
immediate revert when the amount is 0 (which always caused the payment
method selection to snap back in sheet mode).
- `usePredictBuyConditions`: `shouldWaitForPayFees` now also requires
`currentValue > 0`.
- `usePredictBuyActions`: When `approvalRequest` is missing during
`handleConfirm` in PAY_WITH_ANY_TOKEN state, attempts re-initialization
(`initPayWithAnyToken()`) before returning early with error, giving the
user a retry path rather than silently failing. Removed dead fallback
branch that was unreachable (transactionId derived from the same
approvalRequest).
- `usePredictBuyActions`: `batchIdRef` cleared at the start of
`doInit()` to prevent stale values between transaction attempts.
- `usePredictBuyActions`: Sheet unmount cleanup mirrors `beforeRemove`
behavior from the old flow.
- `usePredictBuyActions`: DEPOSITING and SUCCESS state effects check
`isSheetMode` to call `onClose()` instead of `StackActions.pop()`.
- `usePredictBuyError`: Removed `isInputFocused` from the
`blockingPayAlertMessage` gate. The flag was permanently suppressing
insufficient-funds errors in sheet mode because `isInputFocused`
initialized to `true` and never transitioned to `false` when using
quick-amount buttons. The `isPayFeesLoading` check already handles
stale-data concerns.
- `PredictPicks` / `PredictPositionDetail`: Cashout logic (outcome
lookup, `openSellSheet`, error handling with `Logger.error` + toast)
extracted into shared `usePredictCashOut` hook to eliminate duplication.
- `PredictPayWithAnyTokenInfo`: `isInputFocused` overridden to `false`
in sheet mode so `updatePendingAmount` and `setPayToken` run
immediately, ensuring mm_pay relay step is configured before
confirmation.
- `PredictController`: Added `gasFeeToken` to `initPayWithAnyToken`
batch to fix missing mm_pay relay step that caused "Insufficient USDC
balance in Safe" errors.
- `PredictPreviewSheetContext`: Stabilized `onDismiss` callbacks with
`useCallback` to prevent unstable `closeSheet`/`onClose` references from
causing repeated SUCCESS/DEPOSITING effect re-fires.

**Code quality**
- Extracted shared `usePredictCashOut` hook to deduplicate identical
`onCashOut` logic (~40 lines each) from `PredictPicks` and
`PredictPositionDetail`. Both components now use a single hook call
instead of duplicating the guarded action, outcome lookup,
`openSellSheet` call, and error handling with `Logger.error` + toast.
The `callerName` parameter preserves per-component error metadata.
- Replaced raw `View` with design-system `Box` in `PredictQuickAmounts`.
- Removed stale comments from `usePredictBottomSheet`.
- Discriminated union types (`PredictBuyPreviewProps`,
`PredictSellPreviewProps`) replace `Partial<ContentProps>` with `as`
casts, providing compile-time safety for sheet vs navigation mode.
- New test IDs: `BUY_PREVIEW_SHEET`, `SELL_PREVIEW_SHEET`.
- Memoized fallback return value in `usePredictPreviewSheet` to prevent
unstable function references.
- Memoized `BuyComponent` selection in `PredictPreviewSheetContext` with
`useMemo` to prevent unnecessary unmount/remount cycles.
- Added `index.ts` barrel export for `PredictPreviewSheet` component
(file organization standard).
- Restored slide-from-right `cardStyleInterpolator` on `BUY_PREVIEW` and
`SELL_PREVIEW` stack screens for the flag-OFF full-screen path.
- Replaced `require()` with `jest.requireActual()` in all test mock
factories.
- Fixed module-level shared mutable state (`navigationRef`) in
`routes/index.test.tsx` — moved to `beforeEach`.
- Strengthened test assertions in `PredictMarketDetails.test.tsx` to
verify exact call parameters with `expect.objectContaining()`.
- Removed dead/unused `predict.order.buy` i18n key and its stale test
mock reference.
- Migrated `PredictPreviewSheet` from deprecated component-library
`BottomSheet` to `@metamask/design-system-react-native`
`BottomSheet`/`BottomSheetHeader` (DS-first UI rule). Updated
`usePredictBottomSheet` hook to import `BottomSheetRef` from DS.
- Replaced `shouldNavigateBack={false}` with DS equivalent (omit
`goBack` prop); changed header `style` prop to `twClassName`.
- `PredictBuyPreview`: Uses gesture-handler-aware `ScrollView` from
`react-native-gesture-handler` in sheet mode to prevent scroll/dismiss
conflicts with the BottomSheet's `PanGestureHandler` on Android.
- Replaced all `toBeNull()` assertions with `not.toBeOnTheScreen()` in
test files per unit-test guidelines.
- Replaced text-based queries (`getByText`, `queryByText`) with
testID-based assertions (`getByTestId` + `toHaveTextContent`) in
`PredictPreviewSheet.test.tsx` and
`PredictPreviewSheetContext.test.tsx`.
- Changed synchronous `act()` to `await act(async () => { ... })` in
`routes/index.test.tsx` to flush async navigator state updates and
eliminate act() warnings.

**Localization**
- Added `predict.odds` key ("Odds").
- Added `predict.order.confirm` key ("Confirm") for the sheet action
button.
- Removed unused `predict.order.buy` key ("Buy {{outcome}}").

**Documentation**
- Added comprehensive Predictions architecture guide
(`docs/predict/predictions-comprehensive-guide.md`).
- Added ticket documentation
(`docs/predict/tickets/buy-sell-bottomsheet-migration.md`).

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Refs:
[PRED-707](https://consensyssoftware.atlassian.net/browse/PRED-707?atlOrigin=eyJpIjoiZGU2Y2YwN2RlMWJjNDZiNmJkYjdkMmQwMzQ2ZDFmZmMiLCJwIjoiaiJ9)

## **Manual testing steps**

```gherkin
Feature: Predict Buy/Sell BottomSheet Migration

  Scenario: Buy prediction via bottom sheet (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the user is on a prediction market details page

    When user taps a "Yes" or "No" outcome button
    Then a bottom sheet opens with the buy preview content
    And the sheet header shows the outcome icon, "Yes/No · Outcome Name" title, and odds subtitle
    And quick amount buttons ($20, $50, $100, $250) are visible with haptic feedback
    And the user can enter an amount, select payment method, and tap "Confirm"
    And the order is placed successfully
    And the sheet closes after the deposit is initiated

  Scenario: Sell position via bottom sheet (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the user has an active position on a market

    When user taps "Cash out" on their position
    Then a content-fitted bottom sheet opens (not fullscreen)
    And the sheet header shows the position icon, title, and cashout info
    And the body shows the current value, shares, PnL, and Cash out button
    And tapping "Cash out" places the sell order and closes the sheet

  Scenario: Buy prediction via full-screen navigation (flag OFF)
    Given the predictBottomSheet feature flag is disabled
    And the user is on a prediction market details page

    When user taps a "Yes" or "No" outcome button
    Then the app navigates to the full-screen BuyPreview screen
    And the existing layout and behavior are unchanged

  Scenario: Sell position via full-screen navigation (flag OFF)
    Given the predictBottomSheet feature flag is disabled
    And the user has an active position on a market

    When user taps "Cash out" on their position
    Then the app navigates to the full-screen SellPreview screen
    And the existing layout and behavior are unchanged

  Scenario: Change payment method in buy sheet (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the buy bottom sheet is open

    When user taps the Pay with row and selects a different token (e.g. USDC)
    Then the payment method updates and stays selected
    And the fee summary and total update accordingly

  Scenario: Buy from feed cards and carousel (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And the user is on the Predict feed or homepage carousel

    When user taps a Yes/No button on any market card
    Then the bottom sheet opens with the correct market/outcome pre-filled
    And the flow works identically to opening from market details

  Scenario: Components outside PredictScreenStack (flag ON)
    Given the predictBottomSheet feature flag is enabled
    And a Predict card is rendered on the homepage carousel (outside PredictScreenStack)

    When user taps a Yes/No button
    Then the app navigates via React Navigation (fallback behavior)
    And no crash occurs from missing PredictPreviewSheetProvider

  Scenario: Quick amount haptic feedback (flag ON)
    Given the buy bottom sheet is open

    When user taps a quick amount button ($20, $50, $100, or $250)
    Then haptic feedback fires (Light impact)
    And the amount input updates to the selected value
    And the input unfocuses
```

## **Screenshots/Recordings**

### **Before**

Full-screen buy/sell preview navigation (flag OFF) - no visual changes
to this flow.

### **After**



https://github.com/user-attachments/assets/1e365eeb-74bf-4281-a415-0453606c5d80


## **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
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [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.

## **Pre-merge reviewer checklist**

- [x] 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.


[PRED-707]:
https://consensyssoftware.atlassian.net/browse/PRED-707?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches Predict trade entry points and preview/cash-out flows, plus
introduces a new feature-flagged UI layer; regressions could affect
order preview UX or sheet/navigation behavior, though the legacy route
path remains as fallback.
> 
> **Overview**
> Adds a new feature-flagged bet slip flow for Predict by introducing
`PredictPreviewSheet` +
`PredictPreviewSheetProvider`/`usePredictPreviewSheet`, allowing
buy/sell previews to open in BottomSheets (with navigation fallback when
the provider/flag isn’t available).
> 
> Updates multiple Predict entry points (feed cards, carousel cards,
market single/multiple/outcome components, and sport footer) to call
`openBuySheet` instead of navigating to the buy preview, and refactors
cash-out handling into a shared `usePredictCashOut` hook that opens the
sell sheet and handles missing-outcome errors via logging + toast.
> 
> Extends buy preview screens (`PredictBuyPreview`,
`PredictBuyWithAnyToken`) to support a new `mode: 'sheet'` rendering
path (layout tweaks, keypad/header behavior, confirm CTA), adds new test
IDs/feature-flag selectors (`predictBottomSheet`), and adjusts provider
order result shape and assorted tests to match the new behavior.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
ca83caf. 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>
Co-authored-by: Luis Taniça <matallui@gmail.com>
@runway-github runway-github Bot requested a review from a team as a code owner April 16, 2026 22:21
@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.

@metamaskbotv2 metamaskbotv2 Bot added the team-bots Bot team (for MetaMask Bot, Runway Bot, etc.) label Apr 16, 2026
@github-actions github-actions Bot added the risk-high Extensive testing required · High bug introduction risk label Apr 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

⏭️ Smart E2E selection skipped - PR targets a release branch (release/*)

All E2E tests pre-selected.

View GitHub Actions results

@github-actions

Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
11 value mismatches detected (expected — fixture represents an existing user).
View details

@sonarqubecloud

Copy link
Copy Markdown

@chloeYue chloeYue 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

@chloeYue chloeYue merged commit ed06f50 into release/7.74.00 Apr 17, 2026
114 of 115 checks passed
@chloeYue chloeYue deleted the runway-cherry-pick-7.74.0-1776378053 branch April 17, 2026 07:02
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

risk-high Extensive testing required · High bug introduction risk size-XL team-bots Bot team (for MetaMask Bot, Runway Bot, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants