Skip to content

Commit 8f743b4

Browse files
chore(runway): cherry-pick fix(ramps): filter activity tab's transfer details for selected account -> cp-7.71.0 (#27865)
- fix(ramps): filter activity tab's transfer details for selected account -> cp-7.71.0 (#27830) ## **Description** ### Activity — on-ramp orders scoped to the selected account The Activity **Orders** tab merges legacy fiat orders with V2 `RampsController` orders. Legacy rows were already limited to the **selected account group** via `getOrders`. V2 orders were not filtered, so purchase history from other wallets could appear. This change adds `selectRampsOrdersForSelectedAccountGroup`, which keeps only orders whose `walletAddress` matches any formatted address in the selected account group (same semantics as legacy, using `areAddressesEqual` for EVM vs non-EVM). Hook and modal consumers that should reflect “current wallet context” now use this selector instead of the raw controller list. ### Transak — preserve user-entered fiat amount on Build Quote (in-app) After **additional verification**, opening the KYC/payment webview called `navigateToKycWebview` with **`quote.fiatAmount`**, and the stack reset rewrote **RampAmountInput** params with that value. The quote total can differ from what the user typed (e.g. fees), so Build Quote could show **27.37** after the user entered **25**, and that value persisted after closing the sheet or going back. **Change:** `routeAfterAuthentication(..., amount)` already carried the typed fiat; `navigateToAdditionalVerificationCallback` now puts the same `amount` on **both** `RampAmountInput` and `RampAdditionalVerification` route params. **V2 Additional Verification** reads that param and passes **only** it into `navigateToKycWebview` (no `quote.fiatAmount` for this purpose). **Out of scope:** Order detail screens and stored order payloads are unchanged. The **Transak payment webview** URL is unchanged (no `fiatAmount` override in widget params); only in-app Build Quote / stack state matches the user’s input. ## **Changelog** CHANGELOG entry: Fixed Activity on-ramp (Orders) list showing V2 purchases from wallets other than the selected account group; fixed Transak unified buy flow so the fiat amount on Build Quote after additional verification matches the user-entered amount on the amount screen (in-app stack), without changing Transak’s payment widget totals. ### Tests - `selectRampsOrdersForSelectedAccountGroup` and ramp hook consumers (selector + hook unit tests). - `useTransakRouting`: IDPROOF / additional verification navigation with user `amount` set and omitted. - V2 `AdditionalVerification`: continue passes route `amount` into `navigateToKycWebview`. ## **Related issues** Refs: [TRAM-3361](https://consensyssoftware.atlassian.net/browse/TRAM-3361) ## **Manual testing steps** ```gherkin Feature: Activity on-ramp orders and Transak amount (TRAM-3361) Scenario: Orders tab shows only selected account group’s V2 on-ramp orders Given the user has two wallets (or account groups) with separate on-ramp purchase history And unified ramps V2 orders exist for more than one wallet address When the user selects account group A and opens Activity → Orders Then only on-ramp orders whose destination wallet belongs to account group A are listed When the user switches to account group B Then the Orders tab lists only orders for account group B Scenario: Custom fiat amount survives Transak additional verification on Build Quote Given the user is in unified Buy with Transak (native) as provider And the user enters a custom fiat amount (e.g. 25) on the amount screen When the user continues through flows that require additional verification And the user taps Continue on the additional verification screen Then Build Quote under the KYC/payment sheet still shows the entered amount (e.g. 25), not only the quote total When the user closes the webview or goes back from the flow Then the amount screen still shows the same entered amount (e.g. 25) ``` ## **Screenshots/Recordings** ### **Before** <!-- Add video: Activity Orders showing other wallet’s purchases / wrong fiat amount after verification or in widget --> https://github.com/user-attachments/assets/b26bb3cb-8219-48a3-8128-7c79026fdd18 ### **After** <!-- Add video: Activity Orders scoped to selected account / correct custom amount through verification and widget --> https://github.com/user-attachments/assets/3e1929e3-7e1a-42c9-98bd-ea8f7bc9b1eb https://github.com/user-attachments/assets/ef6d14ed-6533-4e04-a5a4-8cbd44477170 ## **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 - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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). - [x] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate UX/data-scoping change: filters which on-ramp orders appear based on multichain account selection and adjusts Transak navigation params, which could hide expected history or affect flow state if account/address resolution is off. > > **Overview** > Fixes unified ramp UI to **scope V2 `RampsController` orders to the selected account group**, preventing purchases from other wallets from showing up in Activity-derived surfaces. This introduces `selectRampsOrdersForSelectedAccountGroup` (address-matched via `areAddressesEqual`) and switches key consumers (`useRampsOrders`, `useRampsProviders`, `useRampsButtonClickData`, `ProviderSelectionModal`) from the unfiltered selector. > > Updates the Transak additional-verification flow to **preserve the user-entered fiat amount** through stack resets: `useTransakRouting` now carries `amount` into `RampAdditionalVerification` params, and `AdditionalVerification` uses that param (not `quote.fiatAmount`) when opening the KYC webview. Selector and routing behavior are covered with expanded unit tests. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 39d5861. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [4661cdb](4661cdb) --------- Co-authored-by: Yu-Gi-Oh! <54774811+imyugioh@users.noreply.github.com>
1 parent 6ebe482 commit 8f743b4

11 files changed

Lines changed: 436 additions & 23 deletions

File tree

app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useRampsController } from '../../../hooks/useRampsController';
1717
import { useRampsQuotes } from '../../../hooks/useRampsQuotes';
1818
import useRampAccountAddress from '../../../hooks/useRampAccountAddress';
1919
import { getOrdersProviders } from '../../../../../../reducers/fiatOrders';
20-
import { selectRampsOrders } from '../../../../../../selectors/rampsController';
20+
import { selectRampsOrdersForSelectedAccountGroup } from '../../../../../../selectors/rampsController';
2121
import { completedOrdersFromRampsOrders } from '../../../utils/determinePreferredProvider';
2222
import { useStyles } from '../../../../../hooks/useStyles';
2323
import styleSheet from './ProviderSelectionModal.styles';
@@ -59,7 +59,9 @@ function ProviderSelectionModal() {
5959
} = useRampsController();
6060

6161
const legacyOrdersProviders = useSelector(getOrdersProviders);
62-
const controllerOrders = useSelector(selectRampsOrders);
62+
const controllerOrders = useSelector(
63+
selectRampsOrdersForSelectedAccountGroup,
64+
);
6365

6466
const ordersProviders = useMemo(() => {
6567
const v2ProviderIds = completedOrdersFromRampsOrders(controllerOrders).map(

app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ jest.mock('../../../../../util/navigation/navUtils', () => ({
3636
(..._args: unknown[]) =>
3737
(params: unknown) => ['MockRoute', params],
3838
useParams: () => ({
39-
quote: { quoteId: 'test-quote-id', fiatAmount: 100 },
39+
quote: { quoteId: 'test-quote-id', fiatAmount: 127.37 },
4040
kycUrl: 'https://kyc.example.com',
4141
workFlowRunId: 'wf-123',
42+
amount: 25,
4243
}),
4344
}));
4445

@@ -71,7 +72,7 @@ describe('V2AdditionalVerification', () => {
7172

7273
expect(mockNavigateToKycWebview).toHaveBeenCalledWith({
7374
kycUrl: 'https://kyc.example.com',
74-
amount: 100,
75+
amount: 25,
7576
});
7677
});
7778

app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ interface V2AdditionalVerificationParams {
2323
quote: TransakBuyQuote;
2424
kycUrl: string;
2525
workFlowRunId: string;
26+
/** From BuildQuote route; keeps stack amount in sync when opening KYC webview. */
27+
amount?: number;
2628
}
2729

2830
const V2AdditionalVerification = () => {
2931
const navigation = useNavigation();
30-
const { kycUrl, quote } = useParams<V2AdditionalVerificationParams>();
32+
const { kycUrl, amount: userEnteredAmount } =
33+
useParams<V2AdditionalVerificationParams>();
3134

3235
const { styles, theme } = useStyles(styleSheet, {});
3336

@@ -46,8 +49,8 @@ const V2AdditionalVerification = () => {
4649
}, [navigation, theme]);
4750

4851
const handleContinuePress = useCallback(() => {
49-
navigateToKycWebview({ kycUrl, amount: quote?.fiatAmount });
50-
}, [navigateToKycWebview, kycUrl, quote?.fiatAmount]);
52+
navigateToKycWebview({ kycUrl, amount: userEnteredAmount });
53+
}, [navigateToKycWebview, kycUrl, userEnteredAmount]);
5154

5255
return (
5356
<ScreenLayout>

app/components/UI/Ramp/hooks/useRampsButtonClickData.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
getRampRoutingDecision,
66
UnifiedRampRoutingType,
77
} from '../../../../reducers/fiatOrders';
8-
import { selectRampsOrders } from '../../../../selectors/rampsController';
8+
import { selectRampsOrdersForSelectedAccountGroup } from '../../../../selectors/rampsController';
99
import { getProviderToken } from '../Deposit/utils/ProviderTokenVault';
1010
import {
1111
completedOrdersFromFiatOrders,
@@ -21,7 +21,9 @@ export interface RampsButtonClickData {
2121

2222
export function useRampsButtonClickData(): RampsButtonClickData {
2323
const orders = useSelector(getOrders);
24-
const controllerOrders = useSelector(selectRampsOrders);
24+
const controllerOrders = useSelector(
25+
selectRampsOrdersForSelectedAccountGroup,
26+
);
2527
const rampRoutingDecision = useSelector(getRampRoutingDecision);
2628
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
2729

app/components/UI/Ramp/hooks/useRampsOrders.test.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,23 @@ import { renderHook, act } from '@testing-library/react-native';
22
import { Provider } from 'react-redux';
33
import { configureStore } from '@reduxjs/toolkit';
44
import React from 'react';
5+
import { AccountGroupType } from '@metamask/account-api';
56
import { RampsOrderStatus, type RampsOrder } from '@metamask/ramps-controller';
7+
import { createMockInternalAccount } from '../../../../util/test/accountsControllerTestUtils';
68
import { useRampsOrders } from './useRampsOrders';
79

10+
const RAMP_HOOKS_TEST_WALLET_ID = 'keyring:use-ramps-orders-test' as const;
11+
const RAMP_HOOKS_TEST_GROUP_ID =
12+
`${RAMP_HOOKS_TEST_WALLET_ID}/ethereum` as const;
13+
const RAMP_HOOKS_TEST_ACCOUNT_ID = 'account-rh-1';
14+
/** Must be a valid EVM address (20 bytes) so `areAddressesEqual` treats it as EVM. */
15+
const RAMP_HOOKS_TEST_ADDRESS = '0x2990079bcdee240329a520d2444386fc119da21a';
16+
17+
const rampHooksTestInternalAccount = {
18+
...createMockInternalAccount(RAMP_HOOKS_TEST_ADDRESS, 'Test'),
19+
id: RAMP_HOOKS_TEST_ACCOUNT_ID,
20+
};
21+
822
const mockAddOrder = jest.fn();
923
const mockAddPrecreatedOrder = jest.fn();
1024
const mockRemoveOrder = jest.fn();
@@ -35,7 +49,7 @@ const createMockOrder = (overrides: Partial<RampsOrder> = {}): RampsOrder => ({
3549
createdAt: Date.now(),
3650
totalFeesFiat: 5,
3751
txHash: '0xabc',
38-
walletAddress: '0x123',
52+
walletAddress: RAMP_HOOKS_TEST_ADDRESS,
3953
status: RampsOrderStatus.Completed,
4054
network: { name: 'Ethereum', chainId: 'eip155:1' },
4155
canBeUpdated: false,
@@ -54,6 +68,45 @@ const createMockStore = (orders: RampsOrder[] = []) =>
5468
RampsController: {
5569
orders,
5670
},
71+
AccountTreeController: {
72+
accountTree: {
73+
wallets: {
74+
[RAMP_HOOKS_TEST_WALLET_ID]: {
75+
id: RAMP_HOOKS_TEST_WALLET_ID,
76+
metadata: { name: 'Test wallet' },
77+
groups: {
78+
[RAMP_HOOKS_TEST_GROUP_ID]: {
79+
id: RAMP_HOOKS_TEST_GROUP_ID,
80+
type: AccountGroupType.SingleAccount,
81+
accounts: [RAMP_HOOKS_TEST_ACCOUNT_ID],
82+
metadata: { name: 'Test Group' },
83+
},
84+
},
85+
},
86+
},
87+
selectedAccountGroup: RAMP_HOOKS_TEST_GROUP_ID,
88+
},
89+
},
90+
RemoteFeatureFlagController: {
91+
remoteFeatureFlags: {
92+
enableMultichainAccounts: {
93+
enabled: true,
94+
featureVersion: '1',
95+
minimumVersion: '1.0.0',
96+
},
97+
},
98+
},
99+
AccountsController: {
100+
internalAccounts: {
101+
accounts: {
102+
[RAMP_HOOKS_TEST_ACCOUNT_ID]: rampHooksTestInternalAccount,
103+
},
104+
selectedAccount: RAMP_HOOKS_TEST_ACCOUNT_ID,
105+
},
106+
},
107+
KeyringController: {
108+
keyrings: [],
109+
},
57110
},
58111
}),
59112
},
@@ -78,7 +131,7 @@ describe('useRampsOrders', () => {
78131
expect(result.current.orders).toEqual([]);
79132
});
80133

81-
it('returns orders from the store', () => {
134+
it('returns orders from the store when walletAddress matches the selected account group', () => {
82135
const order = createMockOrder();
83136
const store = createMockStore([order]);
84137
const { result } = renderHook(() => useRampsOrders(), {
@@ -88,6 +141,19 @@ describe('useRampsOrders', () => {
88141
expect(result.current.orders).toEqual([order]);
89142
});
90143

144+
it('excludes orders whose walletAddress is not in the selected account group', () => {
145+
const foreignOrder = createMockOrder({
146+
providerOrderId: 'foreign-order',
147+
walletAddress: '0x0000000000000000000000000000000000000001',
148+
});
149+
const store = createMockStore([foreignOrder]);
150+
const { result } = renderHook(() => useRampsOrders(), {
151+
wrapper: wrapper(store),
152+
});
153+
154+
expect(result.current.orders).toEqual([]);
155+
});
156+
91157
it('finds an order by providerOrderId', () => {
92158
const order1 = createMockOrder({ providerOrderId: 'order-1' });
93159
const order2 = createMockOrder({ providerOrderId: 'order-2' });

app/components/UI/Ramp/hooks/useRampsOrders.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
33
import type { RampsOrder } from '@metamask/ramps-controller';
44
import { extractOrderCode } from '../utils/extractOrderCode';
55
import Engine from '../../../../core/Engine';
6-
import { selectRampsOrders } from '../../../../selectors/rampsController';
6+
import { selectRampsOrdersForSelectedAccountGroup } from '../../../../selectors/rampsController';
77

88
export interface AddPrecreatedOrderParams {
99
orderId: string;
@@ -31,7 +31,7 @@ export interface UseRampsOrdersResult {
3131
}
3232

3333
export function useRampsOrders(): UseRampsOrdersResult {
34-
const orders = useSelector(selectRampsOrders);
34+
const orders = useSelector(selectRampsOrdersForSelectedAccountGroup);
3535

3636
const getOrderById = useCallback(
3737
(providerOrderId: string) => {

app/components/UI/Ramp/hooks/useRampsProviders.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react';
22
import { useSelector } from 'react-redux';
33
import {
44
selectProviders,
5-
selectRampsOrders,
5+
selectRampsOrdersForSelectedAccountGroup,
66
} from '../../../../selectors/rampsController';
77
import { type Provider } from '@metamask/ramps-controller';
88
import Engine from '../../../../core/Engine';
@@ -55,7 +55,9 @@ export function useRampsProviders(): UseRampsProvidersResult {
5555
} = useSelector(selectProviders);
5656

5757
const legacyOrders = useSelector(getOrders);
58-
const controllerOrders = useSelector(selectRampsOrders);
58+
const controllerOrders = useSelector(
59+
selectRampsOrdersForSelectedAccountGroup,
60+
);
5961

6062
const completedOrders = useMemo(
6163
() => [

app/components/UI/Ramp/hooks/useTransakRouting.test.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ describe('useTransakRouting', () => {
284284
'test-ott',
285285
mockQuote,
286286
MOCK_WALLET_ADDRESS,
287-
expect.any(Object),
287+
{ theme: 'light' },
288288
);
289289
expect(mockReset).toHaveBeenCalledWith(
290290
expect.objectContaining({
@@ -482,10 +482,7 @@ describe('useTransakRouting', () => {
482482
const { result } = renderHook(() => useTransakRouting());
483483

484484
await act(async () => {
485-
await result.current.routeAfterAuthentication(
486-
mockQuote as never,
487-
mockQuote.fiatAmount,
488-
);
485+
await result.current.routeAfterAuthentication(mockQuote as never, 25);
489486
});
490487

491488
expect(mockReset).toHaveBeenCalledWith(
@@ -494,14 +491,64 @@ describe('useTransakRouting', () => {
494491
routes: [
495492
expect.objectContaining({
496493
name: 'RampAmountInput',
497-
params: { amount: mockQuote.fiatAmount },
494+
params: { amount: 25 },
495+
}),
496+
expect.objectContaining({
497+
name: 'RampAdditionalVerification',
498+
params: expect.objectContaining({
499+
quote: mockQuote,
500+
kycUrl: 'https://kyc.example.com',
501+
workFlowRunId: 'wf-123',
502+
amount: 25,
503+
}),
504+
}),
505+
],
506+
}),
507+
);
508+
});
509+
510+
it('handles ADDITIONAL_FORMS_REQUIRED with IDPROOF when user amount is omitted', async () => {
511+
mockGetUserDetails.mockResolvedValue({
512+
firstName: 'John',
513+
address: {},
514+
});
515+
mockGetKycRequirement.mockResolvedValue({
516+
status: 'ADDITIONAL_FORMS_REQUIRED',
517+
kycType: 'STANDARD',
518+
});
519+
mockGetAdditionalRequirements.mockResolvedValue({
520+
formsRequired: [
521+
{
522+
type: 'IDPROOF',
523+
metadata: {
524+
kycUrl: 'https://kyc.example.com',
525+
workFlowRunId: 'wf-123',
526+
},
527+
},
528+
],
529+
});
530+
531+
const { result } = renderHook(() => useTransakRouting());
532+
533+
await act(async () => {
534+
await result.current.routeAfterAuthentication(mockQuote as never);
535+
});
536+
537+
expect(mockReset).toHaveBeenCalledWith(
538+
expect.objectContaining({
539+
index: 1,
540+
routes: [
541+
expect.objectContaining({
542+
name: 'RampAmountInput',
543+
params: { amount: undefined },
498544
}),
499545
expect.objectContaining({
500546
name: 'RampAdditionalVerification',
501547
params: expect.objectContaining({
502548
quote: mockQuote,
503549
kycUrl: 'https://kyc.example.com',
504550
workFlowRunId: 'wf-123',
551+
amount: undefined,
505552
}),
506553
}),
507554
],

app/components/UI/Ramp/hooks/useTransakRouting.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ interface RampStackParamList {
4040
quote: TransakBuyQuote;
4141
kycUrl: string;
4242
workFlowRunId: string;
43+
/** User-entered fiat from BuildQuote; used when resetting stack so amount screen keeps the typed value. */
44+
amount?: number;
4345
};
4446
RampKycProcessing: { quote: TransakBuyQuote };
4547
RampEnterEmail: undefined;
@@ -258,7 +260,7 @@ export const useTransakRouting = (_config?: UseTransakRoutingConfig) => {
258260
},
259261
{
260262
name: Routes.RAMP.ADDITIONAL_VERIFICATION,
261-
params: { quote, kycUrl, workFlowRunId },
263+
params: { quote, kycUrl, workFlowRunId, amount },
262264
},
263265
],
264266
});

0 commit comments

Comments
 (0)