From 816b131820d9c76035f49c91d04eaa18c1efdc97 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Sat, 11 Jul 2026 14:11:07 -0600 Subject: [PATCH 1/2] feat: add Orders home tab for ramps order sync QA Temporary QA surface on the account overview to inspect synced V2 ramps orders and trigger manual User Storage sync during cross-client testing. Co-authored-by: Cursor --- shared/constants/app-state.ts | 1 + .../account-overview-tabs.tsx | 12 ++++ ui/pages/activity/ramps-orders-tab.tsx | 61 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 ui/pages/activity/ramps-orders-tab.tsx diff --git a/shared/constants/app-state.ts b/shared/constants/app-state.ts index 2e6071983169..b2a8ec1cd5bf 100644 --- a/shared/constants/app-state.ts +++ b/shared/constants/app-state.ts @@ -9,6 +9,7 @@ export enum AccountOverviewTabKey { Activity = 'activity', DeFi = 'defi', Perps = 'perps', + Orders = 'orders', } export type AccountOverviewTab = `${AccountOverviewTabKey}`; diff --git a/ui/components/multichain/account-overview/account-overview-tabs.tsx b/ui/components/multichain/account-overview/account-overview-tabs.tsx index ee69037e770d..c888e05905fd 100644 --- a/ui/components/multichain/account-overview/account-overview-tabs.tsx +++ b/ui/components/multichain/account-overview/account-overview-tabs.tsx @@ -52,6 +52,7 @@ import { } from '../../../../shared/lib/ab-testing/configs/perps-tab-badge'; import { useTokenBalances } from '../../../hooks/useTokenBalances'; import { ActivityList as ActivityListV3 } from '../../../pages/activity/activity-list'; +import { RampsOrdersTab } from '../../../pages/activity/ramps-orders-tab'; import { ActivityList as ActivityListV2 } from '../activity-v2/activity-list'; import { usePrefetchTransactions } from '../activity-v2/useTransactionsQuery'; import { getIsActivityListRedesignEnabled } from '../../../selectors/activity/feature-flags'; @@ -136,6 +137,7 @@ export const AccountOverviewTabs = ({ ...(showDefi ? [AccountOverviewTabKey.DeFi] : []), ...(showNfts ? [AccountOverviewTabKey.Nfts] : []), ...(showActivity ? [AccountOverviewTabKey.Activity] : []), + AccountOverviewTabKey.Orders, ]; // Perps is the effective active tab when it is explicitly selected, or when // the active tab isn't rendered and Tabs clamps to the first rendered tab. @@ -351,6 +353,16 @@ export const AccountOverviewTabs = ({ )} + + + + + + ); diff --git a/ui/pages/activity/ramps-orders-tab.tsx b/ui/pages/activity/ramps-orders-tab.tsx new file mode 100644 index 000000000000..021993f46e2f --- /dev/null +++ b/ui/pages/activity/ramps-orders-tab.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { useRampsOrders } from '../../hooks/ramps/useRampsOrders'; +import { useAppSelector } from '../../store/store'; +import { selectRampsOrders } from '../../selectors/rampsController'; +import { syncRampsOrdersWithUserStorage } from '../../store/controller-actions/ramps-controller'; + +export const RampsOrdersTab = () => { + const { orders: accountOrders } = useRampsOrders(); + const allOrders = useAppSelector(selectRampsOrders); + const [syncStatus, setSyncStatus] = useState(null); + + const handleSync = async () => { + setSyncStatus('Syncing...'); + try { + await syncRampsOrdersWithUserStorage(); + setSyncStatus('Sync complete'); + } catch (error) { + setSyncStatus( + `Sync failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }; + + return ( +
+

+ All orders: {allOrders.length} · Selected account: {accountOrders.length} +

+ + {syncStatus ?

{syncStatus}

: null} + {allOrders.length === 0 ? ( +

No ramps orders in controller state.

+ ) : ( + allOrders.map((order) => ( +
+

+ {order.id ?? order.providerOrderId} ·{' '} + {order.status} · {order.provider?.id ?? 'unknown provider'} +

+

+ wallet: {order.walletAddress} · created:{' '} + {order.createdAt + ? new Date(order.createdAt).toLocaleString() + : '—'} +

+
+              {JSON.stringify(order, null, 2)}
+            
+
+ )) + )} +
+ ); +}; + +export default RampsOrdersTab; From 370cfe4a3c3520ca18f09e87d69a919e58714e52 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Sun, 12 Jul 2026 04:27:34 -0600 Subject: [PATCH 2/2] feat(ramps): polish QA orders tab with cards and detail modal Improve the temporary order sync QA surface with clickable order cards, structured detail view, and snapshot tests for easier cross-client testing. Co-authored-by: Cursor --- .../ramps-orders-tab.test.tsx.snap | 462 ++++++++++++++++++ ui/pages/activity/ramps-order-card.tsx | 130 +++++ .../activity/ramps-order-detail-modal.tsx | 103 ++++ ui/pages/activity/ramps-orders-tab.test.tsx | 92 ++++ ui/pages/activity/ramps-orders-tab.tsx | 104 ++-- 5 files changed, 860 insertions(+), 31 deletions(-) create mode 100644 ui/pages/activity/__snapshots__/ramps-orders-tab.test.tsx.snap create mode 100644 ui/pages/activity/ramps-order-card.tsx create mode 100644 ui/pages/activity/ramps-order-detail-modal.tsx create mode 100644 ui/pages/activity/ramps-orders-tab.test.tsx diff --git a/ui/pages/activity/__snapshots__/ramps-orders-tab.test.tsx.snap b/ui/pages/activity/__snapshots__/ramps-orders-tab.test.tsx.snap new file mode 100644 index 000000000000..4cc1ba3eee73 --- /dev/null +++ b/ui/pages/activity/__snapshots__/ramps-orders-tab.test.tsx.snap @@ -0,0 +1,462 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RampsOrdersTab matches snapshot when empty 1`] = ` +
+
+
+

+ Ramps order sync QA +

+

+ All orders: + 0 + · Selected account: + + 0 +

+
+ +
+
+
+

+ No ramps orders in controller state. +

+

+ Use sync above after completing a buy flow on another client. +

+
+
+
+`; + +exports[`RampsOrdersTab matches snapshot with orders 1`] = ` +
+
+
+

+ Ramps order sync QA +

+

+ All orders: + 1 + · Selected account: + + 0 +

+
+ +
+
+
+ +
+
+
+`; + +exports[`RampsOrdersTab opens order details when a card is clicked 1`] = ` +
+ +
+`; diff --git a/ui/pages/activity/ramps-order-card.tsx b/ui/pages/activity/ramps-order-card.tsx new file mode 100644 index 000000000000..1f5f3190dc18 --- /dev/null +++ b/ui/pages/activity/ramps-order-card.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + type RampsOrder, + RampsOrderStatus, + getInternalOrderCode, +} from '@metamask/ramps-controller'; +import { Text, TextVariant } from '@metamask/design-system-react'; + +const STATUS_LABEL_CLASS: Record = { + [RampsOrderStatus.Completed]: 'bg-success-muted text-success-default', + [RampsOrderStatus.Pending]: 'bg-warning-muted text-warning-default', + [RampsOrderStatus.Created]: 'bg-info-muted text-info-default', + [RampsOrderStatus.Precreated]: 'bg-info-muted text-info-default', + [RampsOrderStatus.Failed]: 'bg-error-muted text-error-default', + [RampsOrderStatus.Cancelled]: 'bg-error-muted text-error-default', + [RampsOrderStatus.IdExpired]: 'bg-error-muted text-error-default', +}; + +export function getOrderKey(order: RampsOrder): string { + return getInternalOrderCode(order); +} + +function truncateAddress(address: string): string { + if (address.length <= 12) { + return address; + } + + return `${address.slice(0, 6)}…${address.slice(-4)}`; +} + +export function formatCreatedAt(createdAt?: number): string { + if (!createdAt) { + return '—'; + } + + return new Date(createdAt).toLocaleString(); +} + +export function formatFiatAmount(order: RampsOrder): string { + const symbol = order.fiatCurrency?.symbol ?? order.fiatCurrency?.denomSymbol; + const amount = order.fiatAmount ?? order.fiatAmountInUsd; + + if (amount === undefined || amount === null) { + return '—'; + } + + return symbol ? `${symbol} ${amount}` : String(amount); +} + +export function formatCryptoAmount(order: RampsOrder): string { + const symbol = order.cryptoCurrency?.symbol; + const amount = order.cryptoAmount; + + if (amount === undefined || amount === null || amount === '') { + return '—'; + } + + return symbol ? `${amount} ${symbol}` : String(amount); +} + +function getStatusClassName(status: RampsOrderStatus | string): string { + return STATUS_LABEL_CLASS[status] ?? 'bg-background-muted text-alternative'; +} + +type RampsOrderCardProps = { + order: RampsOrder; + onSelect: (order: RampsOrder) => void; +}; + +export const RampsOrderCard = ({ order, onSelect }: RampsOrderCardProps) => { + const orderKey = getOrderKey(order); + const providerName = + order.provider?.name ?? order.provider?.id ?? 'Unknown provider'; + + return ( + + ); +}; diff --git a/ui/pages/activity/ramps-order-detail-modal.tsx b/ui/pages/activity/ramps-order-detail-modal.tsx new file mode 100644 index 000000000000..58da9fa9122a --- /dev/null +++ b/ui/pages/activity/ramps-order-detail-modal.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import type { RampsOrder } from '@metamask/ramps-controller'; +import { Text, TextVariant } from '@metamask/design-system-react'; +import { + Modal, + ModalContent, + ModalHeader, + ModalOverlay, +} from '../../components/component-library'; +import { + formatCreatedAt, + formatCryptoAmount, + formatFiatAmount, + getOrderKey, +} from './ramps-order-card'; + +type DetailRowProps = { + label: string; + value: React.ReactNode; +}; + +const DetailRow = ({ label, value }: DetailRowProps) => ( +
+ + {label} + + + {value} + +
+); + +type RampsOrderDetailModalProps = { + order: RampsOrder; + onClose: () => void; +}; + +export const RampsOrderDetailModal = ({ + order, + onClose, +}: RampsOrderDetailModalProps) => { + const orderKey = getOrderKey(order); + + return ( + + + + Order details +
+ + + + + + + + + + + + + + Open + + ) : ( + '—' + ) + } + /> +
+ + Raw payload + +
+              {JSON.stringify(order, null, 2)}
+            
+
+
+
+
+ ); +}; diff --git a/ui/pages/activity/ramps-orders-tab.test.tsx b/ui/pages/activity/ramps-orders-tab.test.tsx new file mode 100644 index 000000000000..b42ad3003311 --- /dev/null +++ b/ui/pages/activity/ramps-orders-tab.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { RampsOrderStatus } from '@metamask/ramps-controller'; +import { syncRampsOrdersWithUserStorage } from '../../store/controller-actions/ramps-controller'; +import { + createRampsMockStore, + createRampsTestWrapper, +} from '../../hooks/ramps/test-utils'; +import { RampsOrdersTab } from './ramps-orders-tab'; + +jest.mock('../../store/controller-actions/ramps-controller', () => ({ + syncRampsOrdersWithUserStorage: jest.fn().mockResolvedValue(undefined), +})); + +const mockOrder = { + id: 'order-1', + providerOrderId: 'provider-order-1', + walletAddress: '0x1234567890abcdef1234567890abcdef12345678', + status: RampsOrderStatus.Completed, + createdAt: 1_704_067_200_000, + fiatAmount: 100, + cryptoAmount: '0.05', + fiatCurrency: { symbol: 'USD' }, + cryptoCurrency: { symbol: 'ETH' }, + provider: { id: '/providers/transak', name: 'Transak' }, + network: { name: 'Ethereum', chainId: 'eip155:1' }, + paymentMethod: { id: 'card', name: 'Debit card' }, + orderType: 'BUY', + providerOrderLink: 'https://example.com/order/1', + txHash: '', + isOnlyLink: false, + success: true, + totalFeesFiat: 2, + canBeUpdated: false, + idHasExpired: false, + excludeFromPurchases: false, + timeDescriptionPending: '', +} as const; + +describe('RampsOrdersTab', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('matches snapshot when empty', () => { + const { container } = render(, { + wrapper: createRampsTestWrapper(), + }); + + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot with orders', () => { + const store = createRampsMockStore({ + orders: [mockOrder], + }); + + const { container } = render(, { + wrapper: createRampsTestWrapper(store), + }); + + expect(container).toMatchSnapshot(); + }); + + it('opens order details when a card is clicked', async () => { + const user = userEvent.setup(); + const store = createRampsMockStore({ + orders: [mockOrder], + }); + + render(, { + wrapper: createRampsTestWrapper(store), + }); + + await user.click(screen.getByTestId('ramps-order-card-provider-order-1')); + + expect(screen.getByTestId('ramps-order-detail-modal')).toMatchSnapshot(); + }); + + it('triggers user storage sync', async () => { + const user = userEvent.setup(); + + render(, { + wrapper: createRampsTestWrapper(), + }); + + await user.click(screen.getByTestId('ramps-orders-sync-button')); + + expect(syncRampsOrdersWithUserStorage).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/pages/activity/ramps-orders-tab.tsx b/ui/pages/activity/ramps-orders-tab.tsx index 021993f46e2f..d100a4328205 100644 --- a/ui/pages/activity/ramps-orders-tab.tsx +++ b/ui/pages/activity/ramps-orders-tab.tsx @@ -1,16 +1,32 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; +import type { RampsOrder } from '@metamask/ramps-controller'; +import { Button, Text, TextVariant } from '@metamask/design-system-react'; import { useRampsOrders } from '../../hooks/ramps/useRampsOrders'; import { useAppSelector } from '../../store/store'; import { selectRampsOrders } from '../../selectors/rampsController'; import { syncRampsOrdersWithUserStorage } from '../../store/controller-actions/ramps-controller'; +import { getOrderKey, RampsOrderCard } from './ramps-order-card'; +import { RampsOrderDetailModal } from './ramps-order-detail-modal'; export const RampsOrdersTab = () => { const { orders: accountOrders } = useRampsOrders(); const allOrders = useAppSelector(selectRampsOrders); const [syncStatus, setSyncStatus] = useState(null); + const [selectedOrder, setSelectedOrder] = useState(null); + const [isSyncing, setIsSyncing] = useState(false); + + const sortedOrders = useMemo( + () => + [...allOrders].sort( + (left, right) => (right.createdAt ?? 0) - (left.createdAt ?? 0), + ), + [allOrders], + ); const handleSync = async () => { + setIsSyncing(true); setSyncStatus('Syncing...'); + try { await syncRampsOrdersWithUserStorage(); setSyncStatus('Sync complete'); @@ -18,42 +34,68 @@ export const RampsOrdersTab = () => { setSyncStatus( `Sync failed: ${error instanceof Error ? error.message : String(error)}`, ); + } finally { + setIsSyncing(false); } }; return ( -
-

- All orders: {allOrders.length} · Selected account: {accountOrders.length} -

- - {syncStatus ?

{syncStatus}

: null} - {allOrders.length === 0 ? ( -

No ramps orders in controller state.

- ) : ( - allOrders.map((order) => ( -
+
+ + Ramps order sync QA + + + All orders: {allOrders.length} · Selected account:{' '} + {accountOrders.length} + +
+
- )) + {isSyncing ? 'Syncing…' : 'Sync from User Storage'} + + {syncStatus ? ( + + {syncStatus} + + ) : null} +
+
+ + {sortedOrders.length === 0 ? ( +
+ + No ramps orders in controller state. + + + Use sync above after completing a buy flow on another client. + +
+ ) : ( +
+ {sortedOrders.map((order) => ( + + ))} +
)} + + {selectedOrder ? ( + setSelectedOrder(null)} + /> + ) : null}
); };