Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
149 changes: 148 additions & 1 deletion app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
import { act, fireEvent } from '@testing-library/react-native';
import { PerpsPayRow } from './PerpsPayRow';
import { useNavigation } from '@react-navigation/native';
import { useTransactionPayToken } from '../../../../Views/confirmations/hooks/pay/useTransactionPayToken';
Expand Down Expand Up @@ -300,4 +300,151 @@ describe('PerpsPayRow', () => {
Engine.context.PerpsController?.setSelectedPaymentToken,
).toHaveBeenCalledWith(null);
});

describe('pending config sync (apply once per load)', () => {
it('does not overwrite pay token when user switches token after pending config was applied', () => {
const setPayTokenMock = jest.fn();
const pendingTokenA = {
address: '0xTokenA',
chainId: '0x1',
description: 'Token A',
};
mockUsePerpsSelector.mockReturnValue({
selectedPaymentToken: pendingTokenA,
});
mockUsePerpsPayWithToken.mockReturnValue({
address: pendingTokenA.address,
chainId: pendingTokenA.chainId,
description: pendingTokenA.description,
});
mockUseTransactionPayToken.mockReturnValue({
payToken: { address: '0xOther', chainId: '0xa4b1', symbol: 'Other' },
setPayToken: setPayTokenMock,
} as unknown as ReturnType<typeof useTransactionPayToken>);

const { rerender } = renderWithProvider(
<PerpsPayRow initialAsset="BTC" />,
);

expect(setPayTokenMock).toHaveBeenCalledTimes(1);
expect(setPayTokenMock).toHaveBeenCalledWith({
address: '0xTokenA',
chainId: '0x1',
});

setPayTokenMock.mockClear();
(
Engine.context.PerpsController?.setSelectedPaymentToken as jest.Mock
).mockClear();

const tokenB = {
address: '0xTokenB',
chainId: '0x1',
description: 'Token B',
};
mockUsePerpsPayWithToken.mockReturnValue({
address: tokenB.address,
chainId: tokenB.chainId,
description: tokenB.description,
});
mockUseTransactionPayToken.mockReturnValue({
payToken: {
address: tokenB.address,
chainId: tokenB.chainId,
symbol: 'B',
},
setPayToken: setPayTokenMock,
} as unknown as ReturnType<typeof useTransactionPayToken>);

act(() => {
rerender(<PerpsPayRow initialAsset="BTC" />);
});

expect(setPayTokenMock).not.toHaveBeenCalled();
expect(
Engine.context.PerpsController?.setSelectedPaymentToken,
).not.toHaveBeenCalled();
});

it('re-applies pending config when initialAsset changes', () => {
const setPayTokenMock = jest.fn();
const tokenA = {
address: '0xTokenA',
chainId: '0x1',
description: 'Token A',
};
const tokenB = {
address: '0xTokenB',
chainId: '0x1',
description: 'Token B',
};
mockUsePerpsSelector.mockReturnValue({
selectedPaymentToken: tokenA,
});
mockUsePerpsPayWithToken.mockReturnValue({
address: tokenA.address,
chainId: tokenA.chainId,
description: tokenA.description,
});
mockUseTransactionPayToken.mockReturnValue({
payToken: { address: '0xOther', chainId: '0xa4b1', symbol: 'Other' },
setPayToken: setPayTokenMock,
} as unknown as ReturnType<typeof useTransactionPayToken>);

const { rerender } = renderWithProvider(
<PerpsPayRow initialAsset="BTC" />,
);

expect(setPayTokenMock).toHaveBeenCalledWith({
address: '0xTokenA',
chainId: '0x1',
});

setPayTokenMock.mockClear();

mockUsePerpsSelector.mockReturnValue({
selectedPaymentToken: tokenB,
});
mockUsePerpsPayWithToken.mockReturnValue({
address: tokenB.address,
chainId: tokenB.chainId,
description: tokenB.description,
});
mockUseTransactionPayToken.mockReturnValue({
payToken: { address: '0xOther', chainId: '0xa4b1', symbol: 'Other' },
setPayToken: setPayTokenMock,
} as unknown as ReturnType<typeof useTransactionPayToken>);

act(() => {
rerender(<PerpsPayRow initialAsset="ETH" />);
});

expect(setPayTokenMock).toHaveBeenCalledWith({
address: '0xTokenB',
chainId: '0x1',
});
});

it('does not call setSelectedPaymentToken(null) again when pending has no token and already applied', () => {
mockUsePerpsSelector.mockReturnValue({});
mockUsePerpsPayWithToken.mockReturnValue(null);

const setSelectedPaymentTokenMock = Engine.context.PerpsController
?.setSelectedPaymentToken as jest.Mock;

const { rerender } = renderWithProvider(
<PerpsPayRow initialAsset="BTC" />,
);

expect(setSelectedPaymentTokenMock).toHaveBeenCalledTimes(1);
expect(setSelectedPaymentTokenMock).toHaveBeenCalledWith(null);

setSelectedPaymentTokenMock.mockClear();
act(() => {
rerender(<PerpsPayRow initialAsset="BTC" />);
});

expect(setSelectedPaymentTokenMock).not.toHaveBeenCalled();
});
});
});
54 changes: 38 additions & 16 deletions app/components/UI/Perps/Views/PerpsOrderView/PerpsPayRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CHAIN_IDS } from '@metamask/transaction-controller';
import { useNavigation } from '@react-navigation/native';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { strings } from '../../../../../../locales/i18n';
import Badge, {
Expand Down Expand Up @@ -119,38 +119,60 @@ export const PerpsPayRow = ({

const pendingConfigSelectedPaymentToken = pendingConfig?.selectedPaymentToken;

// Track which pending config we've already applied so we don't overwrite the user's
// in-session token selection. Apply pending config only on initial load or when
// switching asset; otherwise switching tokens in the Pay With modal would flip back.
const appliedPendingTokenRef = useRef<
{ address: string; chainId: string } | null | undefined
>(undefined);
const prevInitialAssetRef = useRef(initialAsset);
if (prevInitialAssetRef.current !== initialAsset) {
prevInitialAssetRef.current = initialAsset;
appliedPendingTokenRef.current = undefined;
}

useEffect(() => {
if (!pendingConfigSelectedPaymentToken) {
Engine.context.PerpsController?.setSelectedPaymentToken?.(null);
}
if (pendingConfigSelectedPaymentToken != null) return;
if (appliedPendingTokenRef.current === null) return;
appliedPendingTokenRef.current = null;
Engine.context.PerpsController?.setSelectedPaymentToken?.(null);
}, [pendingConfigSelectedPaymentToken]);

useEffect(() => {
if (!pendingConfigSelectedPaymentToken || !selectedPaymentToken) {
return;
}
if (!pendingConfigSelectedPaymentToken || !selectedPaymentToken) return;

const pendingAddr = pendingConfigSelectedPaymentToken.address;
const pendingChainId = pendingConfigSelectedPaymentToken.chainId;
const alreadyApplied =
appliedPendingTokenRef.current !== undefined &&
(appliedPendingTokenRef.current === null
? false
: appliedPendingTokenRef.current.address === pendingAddr &&
appliedPendingTokenRef.current.chainId === pendingChainId);
if (alreadyApplied) return;

if (
payToken?.address !== pendingConfigSelectedPaymentToken?.address ||
payToken?.chainId !== pendingConfigSelectedPaymentToken?.chainId
payToken?.address !== pendingAddr ||
payToken?.chainId !== pendingChainId
) {
setPayToken({
address: pendingConfigSelectedPaymentToken.address as Hex,
chainId: pendingConfigSelectedPaymentToken.chainId as Hex,
address: pendingAddr as Hex,
chainId: pendingChainId as Hex,
});

Engine.context.PerpsController?.setSelectedPaymentToken?.({
description: pendingConfigSelectedPaymentToken.description,
address: pendingConfigSelectedPaymentToken.address as Hex,
chainId: pendingConfigSelectedPaymentToken.chainId as Hex,
address: pendingAddr as Hex,
chainId: pendingChainId as Hex,
});
}
appliedPendingTokenRef.current = {
address: pendingAddr,
chainId: pendingChainId,
};
}, [
payToken,
pendingConfigSelectedPaymentToken,
setPayToken,
pendingConfig,
initialAsset,
selectedPaymentToken,
]);

Expand Down
Loading