Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
94 changes: 22 additions & 72 deletions app/components/UI/Ramp/Views/Checkout/Checkout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fireEvent, act, waitFor } from '@testing-library/react-native';
import Checkout from './Checkout';
import renderWithProvider from '../../../../../util/test/renderWithProvider';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import Routes from '../../../../../constants/navigation/Routes';
import { callbackBaseUrl } from '../../Aggregator/sdk';

jest.mock('@react-navigation/native', () => {
Expand Down Expand Up @@ -32,11 +33,6 @@ jest.mock('../../hooks/useRampsOrders', () => ({
useRampsOrders: jest.fn(),
}));

jest.mock('../../hooks/useRampsUnifiedV2Enabled', () => ({
__esModule: true,
default: jest.fn(),
}));

jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
useAnalytics: jest.fn(),
}));
Expand All @@ -51,10 +47,6 @@ jest.mock('../../../../../reducers/fiatOrders', () => ({
getRampRoutingDecision: () => null,
}));

jest.mock('../../utils/v2OrderToast', () => ({
showV2OrderToast: jest.fn(),
}));

jest.mock('../../../../../util/Logger', () => ({
error: jest.fn(),
log: jest.fn(),
Expand Down Expand Up @@ -186,10 +178,6 @@ const mockUseParams = jest.requireMock(
const mockUseRampsOrders = jest.requireMock('../../hooks/useRampsOrders')
.useRampsOrders as jest.Mock;

const mockUseRampsUnifiedV2Enabled = jest.requireMock(
'../../hooks/useRampsUnifiedV2Enabled',
).default as jest.Mock;

const mockUseAnalytics = jest.requireMock(
'../../../../hooks/useAnalytics/useAnalytics',
).useAnalytics as jest.Mock;
Expand All @@ -200,8 +188,6 @@ const mockAddProperties = jest.fn();
const mockBuild = jest.fn();

describe('Checkout', () => {
const mockAddOrder = jest.fn();
const mockGetOrderFromCallback = jest.fn();
const mockAddPrecreatedOrder = jest.fn();
const mockNavigation = {
setOptions: jest.fn(),
Expand All @@ -219,11 +205,8 @@ describe('Checkout', () => {
providerName: 'Test Provider',
});
mockUseRampsOrders.mockReturnValue({
addOrder: mockAddOrder,
getOrderFromCallback: mockGetOrderFromCallback,
addPrecreatedOrder: mockAddPrecreatedOrder,
});
mockUseRampsUnifiedV2Enabled.mockReturnValue(false);
mockUseAnalytics.mockReturnValue({
trackEvent: mockTrackEvent,
createEventBuilder: mockCreateEventBuilder,
Expand Down Expand Up @@ -261,8 +244,7 @@ describe('Checkout', () => {
});
});

expect(mockGetOrderFromCallback).not.toHaveBeenCalled();
expect(mockAddOrder).not.toHaveBeenCalled();
expect(mockNavigation.reset).not.toHaveBeenCalled();
});

it('does not invoke callback handler when hasCallbackFlow is false', async () => {
Expand All @@ -277,8 +259,7 @@ describe('Checkout', () => {
fireEvent.press(getByTestId('trigger-callback-navigation'));
});

expect(mockGetOrderFromCallback).not.toHaveBeenCalled();
expect(mockAddOrder).not.toHaveBeenCalled();
expect(mockNavigation.reset).not.toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -468,26 +449,15 @@ describe('Checkout', () => {
});
});

describe('V2 enabled flow', () => {
it('calls showV2OrderToast when V2 is enabled and callback succeeds', async () => {
const { showV2OrderToast } = jest.requireMock(
'../../utils/v2OrderToast',
) as {
showV2OrderToast: jest.Mock;
};
const mockOrder = {
providerOrderId: 'order-v2-1',
cryptoCurrency: { symbol: 'ETH' },
cryptoAmount: '0.5',
status: 'COMPLETED',
};
mockGetOrderFromCallback.mockResolvedValue(mockOrder);
mockUseRampsUnifiedV2Enabled.mockReturnValue(true);
describe('callback success (unified buy stack)', () => {
it('resets navigation to order details with callback params without fetching the order in Checkout', async () => {
const callbackUrl = `${callbackBaseUrl}?orderId=123`;
mockUseParams.mockReturnValue({
url: 'https://provider.example.com/checkout',
providerName: 'Test',
providerCode: 'moonpay',
walletAddress: '0xabc',
cryptocurrency: 'ETH',
});

const { getByTestId } = renderWithProvider(<Checkout />, {}, true, false);
Expand All @@ -497,41 +467,21 @@ describe('Checkout', () => {
});

await waitFor(() => {
expect(showV2OrderToast).toHaveBeenCalledWith(
expect.objectContaining({
orderId: 'order-v2-1',
cryptocurrency: 'ETH',
}),
);
});
});
});

describe('callback error handling', () => {
it('sets error when getOrderFromCallback returns null', async () => {
mockGetOrderFromCallback.mockResolvedValue(null);
mockUseParams.mockReturnValue({
url: 'https://provider.example.com/checkout',
providerName: 'Test',
providerCode: 'moonpay',
walletAddress: '0xabc',
});

const { getByTestId, getByText } = renderWithProvider(
<Checkout />,
{},
true,
false,
);

await act(async () => {
fireEvent.press(getByTestId('trigger-callback-navigation'));
});

await waitFor(() => {
expect(
getByText('Order could not be retrieved from callback'),
).toBeOnTheScreen();
expect(mockNavigation.reset).toHaveBeenCalledWith({
index: 0,
routes: [
{
name: Routes.RAMP.RAMPS_ORDER_DETAILS,
params: {
callbackUrl,
providerCode: 'moonpay',
walletAddress: '0xabc',
showCloseButton: true,
cryptocurrency: 'ETH',
},
},
],
});
});
});
});
Expand Down
41 changes: 9 additions & 32 deletions app/components/UI/Ramp/Views/Checkout/Checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import {
type BottomSheetRef,
} from '@metamask/design-system-react-native';
import HeaderCompactStandard from '../../../../../component-library/components-temp/HeaderCompactStandard';
import useRampsUnifiedV2Enabled from '../../hooks/useRampsUnifiedV2Enabled';
import { showV2OrderToast } from '../../utils/v2OrderToast';
import { useStyles } from '../../../../hooks/useStyles';
import styleSheet from './Checkout.styles';
import Device from '../../../../../util/device';
Expand Down Expand Up @@ -72,12 +70,9 @@ const Checkout = () => {
const navigation = useNavigation();
const params = useParams<CheckoutParams>();
const { styles } = useStyles(styleSheet, {});
const { addOrder, addPrecreatedOrder, getOrderFromCallback } =
useRampsOrders();
const { addPrecreatedOrder } = useRampsOrders();
const { trackEvent, createEventBuilder } = useAnalytics();
const rampRoutingDecision = useSelector(getRampRoutingDecision);
const isV2Enabled = useRampsUnifiedV2Enabled();

const {
url: uri,
providerCode,
Expand All @@ -87,6 +82,7 @@ const Checkout = () => {
network,
userAgent,
onNavigationStateChange,
cryptocurrency,
} = params ?? {};
const effectiveOrderId = (orderIdParam ?? customOrderId)?.trim() || null;

Expand Down Expand Up @@ -165,37 +161,21 @@ const Checkout = () => {
throw new Error('No wallet address or provider code available');
}

const rampsOrder = await getOrderFromCallback(
providerCode,
navState.url,
walletAddress,
);

if (!rampsOrder) {
throw new Error('Order could not be retrieved from callback');
}

addOrder(rampsOrder);
dispatch(protectWalletModalVisible());

if (isV2Enabled) {
showV2OrderToast({
orderId: rampsOrder.providerOrderId,
cryptocurrency:
rampsOrder.cryptoCurrency?.symbol ?? params?.cryptocurrency ?? '',
cryptoAmount: rampsOrder.cryptoAmount,
status: rampsOrder.status,
});
}

// Unified buy stack only: leave the WebView immediately; OrderDetails
// resolves the order via callback params (same pattern as external-browser return).
navigation.reset({
index: 0,
routes: [
{
name: Routes.RAMP.RAMPS_ORDER_DETAILS,
params: {
orderId: rampsOrder.providerOrderId,
Comment thread
cursor[bot] marked this conversation as resolved.
callbackUrl: navState.url,
providerCode,
walletAddress,
showCloseButton: true,
...(cryptocurrency ? { cryptocurrency } : {}),
},
},
],
Expand All @@ -213,10 +193,7 @@ const Checkout = () => {
providerCode,
walletAddress,
navigation,
addOrder,
getOrderFromCallback,
isV2Enabled,
params?.cryptocurrency,
cryptocurrency,
],
);

Expand Down
51 changes: 51 additions & 0 deletions app/components/UI/Ramp/Views/OrderDetails/OrderDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ jest.mock('../../../../../util/theme', () => {
};
});

jest.mock('../../utils/v2OrderToast', () => ({
showV2OrderToast: jest.fn(),
}));

const mockHandleOrderStatusChangedForMetrics = jest.fn();
jest.mock(
'../../../../../core/Engine/controllers/ramps-controller/event-handlers/analytics',
() => ({
handleOrderStatusChangedForMetrics: (...args: unknown[]) =>
mockHandleOrderStatusChangedForMetrics(...args),
}),
);

const mockTrackEvent = jest.fn();
jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
useAnalytics: () => ({
Expand Down Expand Up @@ -206,6 +219,43 @@ describe('OrderDetails', () => {
expect(mockTrackEvent).toHaveBeenCalled();
});

it('shows V2 order toast when callback fetch succeeds', async () => {
const { showV2OrderToast } = jest.requireMock(
'../../utils/v2OrderToast',
) as { showV2OrderToast: jest.Mock };
const completedOrder = {
providerOrderId: 'ord-cb-1',
status: RampsOrderStatus.Completed,
cryptoCurrency: { symbol: 'ETH' },
cryptoAmount: '0.1',
provider: { id: 'moonpay' },
walletAddress: '0x123',
};
mockUseParams.mockReturnValue({
callbackUrl: 'https://callback.example?x=1',
providerCode: 'moonpay',
walletAddress: '0x123',
});
mockGetOrderById.mockReturnValue(undefined);
mockGetOrderFromCallback.mockResolvedValue(completedOrder);

render();

await waitFor(() => {
expect(showV2OrderToast).toHaveBeenCalledWith(
expect.objectContaining({
orderId: 'ord-cb-1',
cryptocurrency: 'ETH',
}),
);
});

expect(mockHandleOrderStatusChangedForMetrics).toHaveBeenCalledWith({
order: completedOrder,
previousStatus: RampsOrderStatus.Precreated,
});
});

it('shows error state with retry when initial callback fetch fails', async () => {
mockUseParams.mockReturnValue({
callbackUrl: 'metamask://on-ramp/providers/paypal?orderId=abc',
Expand All @@ -222,6 +272,7 @@ describe('OrderDetails', () => {
await waitFor(() => {
expect(getByText('Network request failed')).toBeOnTheScreen();
});
expect(mockHandleOrderStatusChangedForMetrics).not.toHaveBeenCalled();
expect(getByText('ramps_order_details.try_again')).toBeOnTheScreen();

await act(async () => {
Expand Down
Loading
Loading