Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
fb2e0bc
feat: add PayWithBottomSheet component and related functionality
vinistevam May 8, 2026
db74f9f
refactor: remove useMMPayFiatConfig and related feature flag logic fr…
vinistevam May 8, 2026
89b206f
Merge branch 'main' into vs/pay-with-bottom-sheet-skeleton
vinistevam May 8, 2026
a4da98f
refactor: update trailingElement type to ReactElement in PayWithRowCo…
vinistevam May 8, 2026
6bb9ae7
Merge branch 'vs/pay-with-bottom-sheet-skeleton' of github.com:MetaMa…
vinistevam May 8, 2026
ff93b41
feat: implement pay with crypto section and related hooks, tests
vinistevam May 11, 2026
7a79ece
fix ci
vinistevam May 11, 2026
b992716
refactor: remove useMMPayFiatConfig hook and related logic from PayWi…
vinistevam May 11, 2026
6abf05c
feat: integrate selected token functionality into crypto payment section
vinistevam May 12, 2026
6f5208b
Merge branch 'main' into vs/pay-with-crypto-section
vinistevam May 12, 2026
27345ad
Merge branch 'vs/pay-with-crypto-section' into vs/pay-with-selected-t…
vinistevam May 12, 2026
baac1a7
feat: implement useDismissOnPayTokenChange hook to manage navigation …
vinistevam May 12, 2026
12ae98b
update docs
vinistevam May 12, 2026
2467418
Merge branch 'main' into vs/pay-with-crypto-section
vinistevam May 12, 2026
7fdd17a
feat: enhance payment token handling by resolving preferred token bas…
vinistevam May 12, 2026
83d4850
feat: implement last used payment method functionality and related tests
vinistevam May 12, 2026
085b0ff
Merge branch 'main' into vs/pay-with-crypto-section
vinistevam May 12, 2026
a7032ee
Merge branch 'main' into vs/pay-with-crypto-section
vinistevam May 13, 2026
10d383f
Merge branch 'vs/pay-with-crypto-section' into vs/pay-with-selected-t…
vinistevam May 13, 2026
5dd5023
Merge branch 'vs/pay-with-selected-token' into vs/pay-with-last-used
vinistevam May 13, 2026
afe3d52
Merge branch 'main' into vs/pay-with-selected-token
vinistevam May 13, 2026
81ccdef
Merge branch 'vs/pay-with-selected-token' into vs/pay-with-last-used
vinistevam May 13, 2026
13ea3cd
refactor: extract findLatestMetaMaskPayToken function for better reus…
vinistevam May 13, 2026
d2907d0
feat: add temporary feature gate for Pay With bottom sheet UI
vinistevam May 14, 2026
54867b3
feat: implement Pay With Perps section and related functionality
vinistevam May 13, 2026
58870a6
small fixes
vinistevam May 13, 2026
5ba9b00
Merge branch 'main' into vs/pay-with-selected-token
vinistevam May 14, 2026
401c2bf
Merge branch 'vs/pay-with-selected-token' into vs/pay-with-last-used
vinistevam May 14, 2026
6875c0d
Merge branch 'main' into vs/pay-with-last-used
vinistevam May 14, 2026
9f573ad
Merge branch 'vs/pay-with-last-used' into vs/pay-with-perps-section
vinistevam May 14, 2026
78fcae0
Merge branch 'main' into vs/pay-with-perps-section
vinistevam May 15, 2026
f0e090f
feat: enhance payment token handling in Pay With Crypto and Perps sec…
vinistevam May 15, 2026
feeb4dc
Merge branch 'main' into vs/pay-with-perps-section
vinistevam May 15, 2026
fd3e5c2
Merge branch 'main' into vs/pay-with-perps-section
vinistevam May 15, 2026
a48c3de
feat: enhance usePerpsBalanceTokenFilter to apply allowlist filter wi…
vinistevam May 15, 2026
8663914
feat: implement dismissOnSelectCount for improved navigation in PayWi…
vinistevam May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { useIsPerpsBalanceSelected } from '../../hooks/useIsPerpsBalanceSelected
import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking';
import { Hex } from '@metamask/utils';
import { MetaMetricsEvents } from '../../../../../core/Analytics/MetaMetrics.events';
import { isPayWithBottomSheetEnabled } from '../../../../Views/confirmations/utils/transaction-pay';

const tokenIconStyles = StyleSheet.create({
iconSmall: {
Expand Down Expand Up @@ -122,6 +123,10 @@ export const PerpsPayRow = ({
[PERPS_EVENT_PROPERTY.INTERACTION_TYPE]:
PERPS_EVENT_VALUE.INTERACTION_TYPE.PAYMENT_TOKEN_SELECTOR,
});
if (isPayWithBottomSheetEnabled()) {
navigation.navigate(Routes.CONFIRMATION_PAY_WITH_BOTTOM_SHEET);
return;
}
navigation.navigate(Routes.CONFIRMATION_PAY_WITH_MODAL);
}, [canEdit, navigation, setConfirmationMetric, track]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useNavigation } from '@react-navigation/native';
import useApprovalRequest from '../../../Views/confirmations/hooks/useApprovalRequest';
import { selectPerpsAccountState } from '../selectors/perpsController';
import { selectPerpsPayWithAnyTokenAllowlistAssets } from '../selectors/featureFlags';
import { isPayWithBottomSheetEnabled } from '../../../Views/confirmations/utils/transaction-pay';

jest.mock('../../../../../locales/i18n', () => ({
strings: jest.fn((key: string) => key),
Expand All @@ -42,6 +43,10 @@ jest.mock('images/perps-pay-token-icon.png', () => ({
uri: 'perps-pay-token-icon-uri',
}));

jest.mock('../../../Views/confirmations/utils/transaction-pay', () => ({
...jest.requireActual('../../../Views/confirmations/utils/transaction-pay'),
isPayWithBottomSheetEnabled: jest.fn(() => false),
}));
jest.mock('../../../UI/SimulationDetails/FiatDisplay/useFiatFormatter', () =>
jest.fn(
() => (value: { toNumber: () => number }) =>
Expand Down Expand Up @@ -70,6 +75,10 @@ const mockUseNavigation = useNavigation as jest.MockedFunction<
const mockUseApprovalRequest = useApprovalRequest as jest.MockedFunction<
typeof useApprovalRequest
>;
const mockIsPayWithBottomSheetEnabled =
isPayWithBottomSheetEnabled as jest.MockedFunction<
typeof isPayWithBottomSheetEnabled
>;

describe('usePerpsBalanceTokenFilter', () => {
const chainId = '0xa4b1';
Expand All @@ -80,6 +89,7 @@ describe('usePerpsBalanceTokenFilter', () => {

beforeEach(() => {
jest.clearAllMocks();
mockIsPayWithBottomSheetEnabled.mockReturnValue(false);
mockUseTransactionMetadataRequest.mockReturnValue(undefined);
mockUseIsPerpsBalanceSelected.mockReturnValue(false);
mockUseSelector.mockImplementation(
Expand Down Expand Up @@ -401,5 +411,62 @@ describe('usePerpsBalanceTokenFilter', () => {
expect(mockOnPerpsPaymentTokenChange).toHaveBeenCalledWith(null);
}
});

it('omits the synthetic perps balance row when the new Pay With bottom sheet is enabled', () => {
mockIsPayWithBottomSheetEnabled.mockReturnValue(true);
const inputTokens: AssetType[] = [
{
address: '0xabc',
chainId,
symbol: 'USDC',
name: 'USD Coin',
balance: '100',
} as AssetType,
];

const { result } = renderHook(() => usePerpsBalanceTokenFilter());
const output = result.current(inputTokens);

expect(output).toHaveLength(1);
expect(isHighlightedItemOutsideAssetList(output[0])).toBe(false);
expect((output[0] as AssetType).address).toBe('0xabc');
});

it('still applies the allowlist filter when the new Pay With bottom sheet is enabled', () => {
mockIsPayWithBottomSheetEnabled.mockReturnValue(true);
const allowlistKey = `${chainId}.0xusdc`.toLowerCase();
mockUseSelector.mockImplementation(
(selector: (state: unknown) => unknown) => {
if (selector === selectPerpsAccountState)
return { spendableBalance: '100.00' };
if (selector === selectPerpsPayWithAnyTokenAllowlistAssets)
return [allowlistKey];
return [];
},
);
const inputTokens: AssetType[] = [
{
address: '0xusdc',
chainId,
symbol: 'USDC',
name: 'USD Coin',
balance: '500',
} as AssetType,
{
address: '0xother',
chainId,
symbol: 'OTHER',
name: 'Other',
balance: '100',
} as AssetType,
];

const { result } = renderHook(() => usePerpsBalanceTokenFilter());
const output = result.current(inputTokens);

expect(output).toHaveLength(1);
expect(isHighlightedItemOutsideAssetList(output[0])).toBe(false);
expect((output[0] as AssetType).address).toBe('0xusdc');
});
});
});
5 changes: 5 additions & 0 deletions app/components/UI/Perps/hooks/usePerpsBalanceTokenFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
HighlightedItem,
type TokenListItem,
} from '../../../Views/confirmations/types/token';
import { isPayWithBottomSheetEnabled } from '../../../Views/confirmations/utils/transaction-pay';
import { hasTransactionType } from '../../../Views/confirmations/utils/transaction';
import { selectPerpsPayWithAnyTokenAllowlistAssets } from '../selectors/featureFlags';
import { selectPerpsAccountState } from '../selectors/perpsController';
Expand Down Expand Up @@ -108,6 +109,10 @@ export function usePerpsBalanceTokenFilter(): (
return mappedTokens;
}

if (isPayWithBottomSheetEnabled()) {
return mappedTokens;
}

const highlightedAction: HighlightedItem = {
position: 'outside_of_asset_list',
icon: PERPS_BALANCE_ICON_URI,
Expand Down
9 changes: 7 additions & 2 deletions app/components/UI/Perps/hooks/usePerpsPaymentToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import { useTransactionPayToken } from '../../../Views/confirmations/hooks/pay/u
import Engine from '../../../../core/Engine';
import { parsePayWithToken } from '../utils/parsePayWithToken';

export type PerpsPaymentTokenInput =
| AssetType
| { address: string; chainId: string }
| null;

export interface UsePerpsPaymentTokenResult {
onPaymentTokenChange: (token: AssetType | null) => void;
onPaymentTokenChange: (token: PerpsPaymentTokenInput) => void;
}

export function usePerpsPaymentToken(): UsePerpsPaymentTokenResult {
const { setPayToken } = useTransactionPayToken();

const onPaymentTokenChange = useCallback(
(token: AssetType | null) => {
(token: PerpsPaymentTokenInput) => {
const parsed =
token === null || token === undefined ? null : parsePayWithToken(token);

Expand Down
9 changes: 9 additions & 0 deletions app/components/UI/Perps/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { HIP3DebugView } from '../Debug';
import PerpsCrossMarginWarningBottomSheet from '../components/PerpsCrossMarginWarningBottomSheet';
import PerpsSelectProviderView from '../Views/PerpsSelectProviderView';
import { PayWithModal } from '../../../Views/confirmations/components/modals/pay-with-modal/pay-with-modal';
import { PayWithBottomSheet } from '../../../Views/confirmations/components/modals/pay-with-bottom-sheet/pay-with-bottom-sheet';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
/* eslint-disable-next-line */
import { NavigationContext } from '@react-navigation/core';
Expand Down Expand Up @@ -441,6 +442,14 @@ const PerpsScreenStack = () => {
...transparentModalScreenOptions,
}}
/>
<Stack.Screen
name={Routes.CONFIRMATION_PAY_WITH_BOTTOM_SHEET}
component={PayWithBottomSheet}
options={{
headerShown: false,
...transparentModalScreenOptions,
}}
/>

{/* Order redirect screen - handles one-click trade from token details */}
<Stack.Screen
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { Hex } from '@metamask/utils';
import { StackActions, useNavigation } from '@react-navigation/native';
import Engine from '../../../../../../core/Engine';
import { useParams } from '../../../../../../util/navigation/navUtils';
import { useTransactionPayToken } from '../../../hooks/pay/useTransactionPayToken';
import { useTransactionPayWithdraw } from '../../../hooks/pay/useTransactionPayWithdraw';
import { useWithdrawTokenFilter } from '../../../hooks/pay/useWithdrawTokenFilter';
Expand Down Expand Up @@ -40,7 +42,22 @@ import { usePerpsPaymentToken } from '../../../../../UI/Perps/hooks/usePerpsPaym
import { usePredictBalanceTokenFilter } from '../../../../../UI/Predict/hooks/usePredictBalanceTokenFilter';
import { usePredictPaymentToken } from '../../../../../UI/Predict/hooks/usePredictPaymentToken';

interface PayWithModalParams {
/**
* When > 1, PayWithModal owns navigation on close by dispatching
* `StackActions.pop(N)` atomically instead of relying on the legacy
* `BottomSheet`'s built-in `navigation.goBack()`. Set to 2 by the new Pay
* With bottom sheet's "Other assets" launcher so picking a token pops both
* this modal AND the bottom sheet underneath in a single navigator
* dispatch — avoids the Android view-hierarchy race that crashes with
* `IllegalStateException` on two adjacent pops.
*/
dismissOnSelectCount?: number;
}

export function PayWithModal() {
const navigation = useNavigation();
const { dismissOnSelectCount = 1 } = useParams<PayWithModalParams>({});
const transactionMeta = useTransactionMetadataRequest();
const hideNetworkFilter = hasTransactionType(
transactionMeta,
Expand Down Expand Up @@ -114,6 +131,10 @@ export function PayWithModal() {
const handleTokenSelect = useCallback(
(token: AssetType) => {
const onClosed = async () => {
if (dismissOnSelectCount > 1) {
navigation.dispatch(StackActions.pop(dismissOnSelectCount));
}

if (
hasTransactionType(transactionMeta, [TransactionType.musdConversion])
) {
Expand Down Expand Up @@ -171,8 +192,10 @@ export function PayWithModal() {
},
[
close,
dismissOnSelectCount,
isPredictContext,
isWithdraw,
navigation,
onMusdPaymentTokenChange,
onPerpsPaymentTokenChange,
onPredictPaymentTokenChange,
Expand Down Expand Up @@ -245,6 +268,7 @@ export function PayWithModal() {
isFullscreen
ref={bottomSheetRef}
keyboardAvoidingViewEnabled={false}
shouldNavigateBack={dismissOnSelectCount <= 1}
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.

X button fails to dismiss modal when dismissOnSelectCount exceeds one

Medium Severity

When dismissOnSelectCount is greater than 1 (i.e. when opened from "Other assets" in the new PayWithBottomSheet), shouldNavigateBack is false, which prevents the BottomSheet from calling goBack() on any dismiss action. Token selection works because the onClosed callback dispatches StackActions.pop(N). However, the X-button close handler calls close() without a callback, so after the close animation completes, no navigation occurs — the PayWithModal screen stays in the navigation stack invisibly, causing stack pollution and potential navigation issues on subsequent interactions.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8663914. Configure here.

>
<HeaderCompactStandard
title={modalTitle}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { usePayWithCryptoSection } from './usePayWithCryptoSection';
export { usePayWithFiatSection } from './usePayWithFiatSection';
export { usePayWithPerpsSection } from './usePayWithPerpsSection';
Loading
Loading