Skip to content

Commit 4265a15

Browse files
authored
feat(ramp): persist Transak native policy agreement for UB2 [TRAM-3340] (#28952)
## **Description** Unified Buy V2 (Transak native) showed the verify-identity explainer on every visit when the user had no or expired Transak token ([TRAM-3340](https://consensyssoftware.atlassian.net/browse/TRAM-3340)). This change stores **`hasAgreedTransakNativePolicy`** on persisted Redux **`fiatOrders`**. When the user taps Continue on the explainer, we dispatch **`setHasAgreedTransakNativePolicy(true)`**. On later continues with no token, **`BuildQuote`** routes straight to **Enter Email** if **`selectHasAgreedTransakNativePolicy`** is true. ## **Changelog** CHANGELOG entry: Unified Buy users who use Transak native only see the verify-identity policy screen once; later visits skip to email entry when not logged in to Transak. ## **Related issues** Fixes: Refs: https://consensyssoftware.atlassian.net/browse/TRAM-3340 ## **Manual testing steps** ```gherkin Feature: Transak native verify-identity explainer (Unified Buy V2) Scenario: First visit without Transak token shows explainer then email Given the user has not agreed the Transak native policy before And the user selects Transak native on Unified Buy amount screen When the user continues without a Transak session Then the verify-identity explainer is shown When the user taps Continue on the explainer Then the Enter Email screen is shown Scenario: Later visit skips explainer after agreement Given the user has already agreed the Transak native policy And the user has no valid Transak token When the user continues from Unified Buy amount with Transak native Then the Enter Email screen is shown without the explainer ``` ## **Screenshots/Recordings** ### **Before** N/A ### **After** https://github.com/user-attachments/assets/2bad4091-15a6-4c38-9a34-778c0a30b00e ## **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 - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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. [TRAM-3340]: https://consensyssoftware.atlassian.net/browse/TRAM-3340?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, isolated change to Unified Buy navigation and Redux state with test coverage; main risk is incorrect routing if the persisted flag is set/cleared unexpectedly. > > **Overview** > Unified Buy V2 now persists whether the user has agreed to the Transak-native policy explainer via a new `fiatOrders.hasAgreedTransakNativePolicy` flag, plus `setHasAgreedTransakNativePolicy` and `selectHasAgreedTransakNativePolicy`. > > `VerifyIdentity` dispatches the consent on Continue, and `BuildQuote` uses the selector to route native-provider users without a token directly to `EnterEmail` instead of repeatedly showing `VerifyIdentity`. Tests were updated/added to cover the new reducer state, selector behavior, and the updated navigation paths. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 91e8cd1. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a669687 commit 4265a15

7 files changed

Lines changed: 156 additions & 7 deletions

File tree

app/components/UI/Ramp/Views/BuildQuote/BuildQuote.test.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
130130
jest.mock('../../../../../reducers/fiatOrders', () => ({
131131
getRampRoutingDecision: () => 'AGGREGATOR',
132132
UnifiedRampRoutingType: { AGGREGATOR: 'AGGREGATOR' },
133+
selectHasAgreedTransakNativePolicy: jest.fn(() => false),
133134
}));
134135

135136
jest.mock('../../hooks/useRampAccountAddress', () => ({
@@ -214,6 +215,12 @@ const mockUseDebouncedValue = jest.requireMock(
214215
'../../../../hooks/useDebouncedValue',
215216
).useDebouncedValue as jest.Mock;
216217

218+
const mockFiatOrdersModule = jest.requireMock(
219+
'../../../../../reducers/fiatOrders',
220+
) as {
221+
selectHasAgreedTransakNativePolicy: jest.Mock;
222+
};
223+
217224
const mockDeviceIsAndroid = jest.requireMock('../../../../../util/device')
218225
.isAndroid as jest.Mock;
219226

@@ -416,6 +423,9 @@ const buildRampsControllerResult = (overrides = {}) => ({
416423
describe('BuildQuote', () => {
417424
beforeEach(() => {
418425
jest.clearAllMocks();
426+
mockFiatOrdersModule.selectHasAgreedTransakNativePolicy.mockReturnValue(
427+
false,
428+
);
419429
mockUseParams.mockReturnValue({});
420430
mockUseRampsController.mockReturnValue(buildRampsControllerResult());
421431
mockUseDebouncedValue.mockImplementation((value: number) => value);
@@ -1290,7 +1300,35 @@ describe('BuildQuote', () => {
12901300
expect(mockGetBuyQuote).not.toHaveBeenCalled();
12911301
expect(mockRouteAfterAuth).not.toHaveBeenCalled();
12921302
expect(mockNavigate).toHaveBeenCalledWith(
1293-
expect.any(String),
1303+
Routes.RAMP.VERIFY_IDENTITY,
1304+
expect.objectContaining({
1305+
amount: '100',
1306+
currency: 'USD',
1307+
assetId: 'eip155:1/slip44:60',
1308+
}),
1309+
);
1310+
});
1311+
1312+
it('navigates to Enter Email for native provider without token after policy agreement', async () => {
1313+
mockCheckExistingToken.mockResolvedValue(false);
1314+
1315+
mockFiatOrdersModule.selectHasAgreedTransakNativePolicy.mockReturnValue(
1316+
true,
1317+
);
1318+
1319+
const { getByTestId } = renderWithProvider(<BuildQuote />, {
1320+
state: initialRootState,
1321+
});
1322+
1323+
await act(async () => {
1324+
fireEvent.press(getByTestId(BuildQuoteSelectors.CONTINUE_BUTTON));
1325+
});
1326+
1327+
expect(mockCheckExistingToken).toHaveBeenCalled();
1328+
expect(mockGetBuyQuote).not.toHaveBeenCalled();
1329+
expect(mockRouteAfterAuth).not.toHaveBeenCalled();
1330+
expect(mockNavigate).toHaveBeenCalledWith(
1331+
Routes.RAMP.ENTER_EMAIL,
12941332
expect.objectContaining({
12951333
amount: '100',
12961334
currency: 'USD',

app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@ import { BannerAlertSeverity } from '../../../../../component-library/components
7575
import { useTransakController } from '../../hooks/useTransakController';
7676
import { useTransakRouting } from '../../hooks/useTransakRouting';
7777
import { createV2VerifyIdentityNavDetails } from '../NativeFlow/VerifyIdentity';
78+
import { createV2EnterEmailNavDetails } from '../NativeFlow/EnterEmail';
7879
import { parseUserFacingError } from '../../utils/parseUserFacingError';
7980
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
8081
import { MetaMetricsEvents } from '../../../../../core/Analytics';
8182
import { useSelector } from 'react-redux';
8283
import {
8384
getRampRoutingDecision,
85+
selectHasAgreedTransakNativePolicy,
8486
UnifiedRampRoutingType,
8587
} from '../../../../../reducers/fiatOrders';
8688
import { selectProviderAutoSelected } from '../../../../../selectors/rampsController';
@@ -189,6 +191,9 @@ function BuildQuote() {
189191

190192
const { trackEvent, createEventBuilder } = useAnalytics();
191193
const rampRoutingDecision = useSelector(getRampRoutingDecision);
194+
const hasAgreedTransakNativePolicy = useSelector(
195+
selectHasAgreedTransakNativePolicy,
196+
);
192197
const providerAutoSelected = useSelector(selectProviderAutoSelected);
193198
const prevSelectedProviderRef = useRef(selectedProvider);
194199

@@ -645,6 +650,14 @@ function BuildQuote() {
645650
throw new Error(strings('deposit.buildQuote.unexpectedError'));
646651
}
647652
await transakRouteAfterAuth(quote, amountAsNumber);
653+
} else if (hasAgreedTransakNativePolicy) {
654+
navigation.navigate(
655+
...createV2EnterEmailNavDetails({
656+
amount: String(amountAsNumber),
657+
currency,
658+
assetId: selectedToken?.assetId,
659+
}),
660+
);
648661
} else {
649662
navigation.navigate(
650663
...createV2VerifyIdentityNavDetails({
@@ -675,6 +688,7 @@ function BuildQuote() {
675688
transakGetBuyQuote,
676689
transakRouteAfterAuth,
677690
navigation,
691+
hasAgreedTransakNativePolicy,
678692
]);
679693

680694
/**

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
import React from 'react';
22
import { render, fireEvent } from '@testing-library/react-native';
3+
import { useDispatch } from 'react-redux';
34
import V2VerifyIdentity from './VerifyIdentity';
45
import { ThemeContext, mockTheme } from '../../../../../util/theme';
56
import { Linking } from 'react-native';
7+
import { setHasAgreedTransakNativePolicy } from '../../../../../reducers/fiatOrders';
8+
import { VerifyIdentitySelectorsIDs } from './VerifyIdentity.testIds';
9+
10+
jest.mock('react-redux', () => ({
11+
...jest.requireActual('react-redux'),
12+
useDispatch: jest.fn(),
13+
}));
14+
15+
const mockedUseDispatch = useDispatch as jest.MockedFunction<
16+
typeof useDispatch
17+
>;
618

719
const mockNavigate = jest.fn();
820
const mockGoBack = jest.fn();
@@ -66,8 +78,12 @@ const renderWithTheme = (component: React.ReactElement) =>
6678
);
6779

6880
describe('V2VerifyIdentity', () => {
81+
let innerDispatch: jest.Mock;
82+
6983
beforeEach(() => {
7084
jest.clearAllMocks();
85+
innerDispatch = jest.fn();
86+
mockedUseDispatch.mockReturnValue(innerDispatch);
7187
});
7288

7389
it('calls navigation.goBack when header back is pressed', () => {
@@ -79,11 +95,14 @@ describe('V2VerifyIdentity', () => {
7995
expect(mockTrackEvent).toHaveBeenCalled();
8096
});
8197

82-
it('navigates to enter email when submit button is pressed', async () => {
83-
const { getByText } = renderWithTheme(<V2VerifyIdentity />);
98+
it('dispatches Transak native policy agreement and navigates to Enter Email on continue', () => {
99+
const { getByTestId } = renderWithTheme(<V2VerifyIdentity />);
84100

85-
fireEvent.press(getByText('deposit.verify_identity.button'));
101+
fireEvent.press(getByTestId(VerifyIdentitySelectorsIDs.CONTINUE_BUTTON));
86102

103+
expect(innerDispatch).toHaveBeenCalledWith(
104+
setHasAgreedTransakNativePolicy(true),
105+
);
87106
expect(mockNavigate).toHaveBeenCalledWith(
88107
'RampEnterEmail',
89108
expect.objectContaining({

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import {
3030
createNavigationDetails,
3131
useParams,
3232
} from '../../../../../util/navigation/navUtils';
33+
import { useDispatch } from 'react-redux';
3334
import { createV2EnterEmailNavDetails } from './EnterEmail';
3435
import { VerifyIdentitySelectorsIDs } from './VerifyIdentity.testIds';
36+
import { setHasAgreedTransakNativePolicy } from '../../../../../reducers/fiatOrders';
3537

3638
export interface V2VerifyIdentityParams {
3739
amount?: string;
@@ -43,6 +45,7 @@ export const createV2VerifyIdentityNavDetails =
4345
createNavigationDetails<V2VerifyIdentityParams>(Routes.RAMP.VERIFY_IDENTITY);
4446

4547
const V2VerifyIdentity = () => {
48+
const dispatch = useDispatch();
4649
const navigation = useNavigation();
4750
const { styles } = useStyles(styleSheet, {});
4851
const { trackEvent, createEventBuilder } = useAnalytics();
@@ -83,7 +86,7 @@ const V2VerifyIdentity = () => {
8386
);
8487
}, [trackEvent, createEventBuilder]);
8588

86-
const handleSubmit = useCallback(async () => {
89+
const handleSubmit = useCallback(() => {
8790
trackEvent(
8891
createEventBuilder(MetaMetricsEvents.RAMPS_TERMS_CONSENT_CLICKED)
8992
.addProperties({
@@ -92,8 +95,9 @@ const V2VerifyIdentity = () => {
9295
})
9396
.build(),
9497
);
98+
dispatch(setHasAgreedTransakNativePolicy(true));
9599
navigateToEnterEmail();
96-
}, [navigateToEnterEmail, trackEvent, createEventBuilder]);
100+
}, [dispatch, navigateToEnterEmail, trackEvent, createEventBuilder]);
97101

98102
const handleTransakLink = useCallback(() => {
99103
let urlDomain: string = TRANSAK_URL;

app/reducers/fiatOrders/index.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ import fiatOrderReducer, {
5959
getDetectedGeolocation,
6060
getRampRoutingDecision,
6161
setRampRoutingDecision,
62+
setHasAgreedTransakNativePolicy,
63+
selectHasAgreedTransakNativePolicy,
6264
UnifiedRampRoutingType,
6365
} from '.';
6466
import { FIAT_ORDER_PROVIDERS } from '../../constants/on-ramp';
@@ -417,6 +419,29 @@ describe('fiatOrderReducer', () => {
417419
expect(stateWithStartedFalse.getStartedDeposit).toEqual(false);
418420
});
419421

422+
it('sets hasAgreedTransakNativePolicy to true', () => {
423+
const next = fiatOrderReducer(
424+
initialState,
425+
setHasAgreedTransakNativePolicy(true),
426+
);
427+
428+
expect(next.hasAgreedTransakNativePolicy).toEqual(true);
429+
});
430+
431+
it('sets hasAgreedTransakNativePolicy to false', () => {
432+
const agreedState = {
433+
...initialState,
434+
hasAgreedTransakNativePolicy: true,
435+
};
436+
437+
const next = fiatOrderReducer(
438+
agreedState,
439+
setHasAgreedTransakNativePolicy(false),
440+
);
441+
442+
expect(next.hasAgreedTransakNativePolicy).toEqual(false);
443+
});
444+
420445
it('should set the selected region', () => {
421446
const testRegion = {
422447
id: 'test-region',
@@ -1090,6 +1115,34 @@ describe('selectors', () => {
10901115
});
10911116
});
10921117

1118+
describe('selectHasAgreedTransakNativePolicy', () => {
1119+
it('returns true for state with hasAgreedTransakNativePolicy true', () => {
1120+
const state = merge({}, initialRootState, {
1121+
fiatOrders: {
1122+
hasAgreedTransakNativePolicy: true,
1123+
},
1124+
});
1125+
1126+
expect(selectHasAgreedTransakNativePolicy(state)).toEqual(true);
1127+
});
1128+
1129+
it('returns false for state with hasAgreedTransakNativePolicy false', () => {
1130+
const state = merge({}, initialRootState, {
1131+
fiatOrders: {
1132+
hasAgreedTransakNativePolicy: false,
1133+
},
1134+
});
1135+
1136+
expect(selectHasAgreedTransakNativePolicy(state)).toEqual(false);
1137+
});
1138+
1139+
it('returns false for initial root state default fiatOrders', () => {
1140+
expect(selectHasAgreedTransakNativePolicy(initialRootState)).toEqual(
1141+
false,
1142+
);
1143+
});
1144+
});
1145+
10931146
describe('getOrders', () => {
10941147
it('should return empty array if order property is not defined', () => {
10951148
const state = merge({}, initialRootState, {

app/reducers/fiatOrders/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ export const setRampRoutingDecision = (
143143
payload: routingDecision,
144144
});
145145

146+
export const setHasAgreedTransakNativePolicy = (hasAgreed: boolean) => ({
147+
type: ACTIONS.FIAT_SET_HAS_AGREED_TRANSAK_NATIVE_POLICY,
148+
payload: hasAgreed,
149+
});
150+
146151
/**
147152
* Selectors
148153
*/
@@ -223,6 +228,9 @@ export const fiatOrdersGetStartedDeposit: (
223228
) => FiatOrdersState['getStartedDeposit'] = (state: RootState) =>
224229
state.fiatOrders.getStartedDeposit;
225230

231+
export const selectHasAgreedTransakNativePolicy = (state: RootState): boolean =>
232+
state.fiatOrders.hasAgreedTransakNativePolicy === true;
233+
226234
export const getOrdersProviders = createSelector(ordersSelector, (orders) => {
227235
const providers = orders
228236
.filter(
@@ -343,6 +351,7 @@ export const initialState: FiatOrdersState = {
343351
getStartedAgg: false,
344352
getStartedSell: false,
345353
getStartedDeposit: false,
354+
hasAgreedTransakNativePolicy: false,
346355
authenticationUrls: [],
347356
activationKeys: [],
348357
rampRoutingDecision: null,
@@ -640,6 +649,12 @@ const fiatOrderReducer: (
640649
rampRoutingDecision: action.payload,
641650
};
642651
}
652+
case ACTIONS.FIAT_SET_HAS_AGREED_TRANSAK_NATIVE_POLICY: {
653+
return {
654+
...state,
655+
hasAgreedTransakNativePolicy: action.payload,
656+
};
657+
}
643658

644659
default: {
645660
return state;

app/reducers/fiatOrders/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
setFiatSellTxHash,
3737
removeFiatSellTxHash,
3838
setRampRoutingDecision,
39+
setHasAgreedTransakNativePolicy,
3940
} from '.';
4041
import {
4142
FIAT_ORDER_PROVIDERS,
@@ -103,6 +104,8 @@ export interface FiatOrdersState {
103104
getStartedAgg: boolean;
104105
getStartedSell: boolean;
105106
getStartedDeposit: boolean;
107+
/** Unified Buy / Transak native: user agreed to policy copy on the verify-identity explainer screen */
108+
hasAgreedTransakNativePolicy: boolean;
106109
authenticationUrls: string[];
107110
activationKeys: ActivationKey[];
108111
rampRoutingDecision: UnifiedRampRoutingType | null;
@@ -135,6 +138,8 @@ export const ACTIONS = {
135138
FIAT_SET_SELL_TX_HASH: 'FIAT_SET_SELL_TX_HASH',
136139
FIAT_REMOVE_SELL_TX_HASH: 'FIAT_REMOVE_SELL_TX_HASH',
137140
FIAT_SET_RAMP_ROUTING_DECISION: 'FIAT_SET_RAMP_ROUTING_DECISION',
141+
FIAT_SET_HAS_AGREED_TRANSAK_NATIVE_POLICY:
142+
'FIAT_SET_HAS_AGREED_TRANSAK_NATIVE_POLICY',
138143
} as const;
139144

140145
export type Action =
@@ -161,7 +166,8 @@ export type Action =
161166
| ReturnType<typeof updateOnRampNetworks>
162167
| ReturnType<typeof setFiatSellTxHash>
163168
| ReturnType<typeof removeFiatSellTxHash>
164-
| ReturnType<typeof setRampRoutingDecision>;
169+
| ReturnType<typeof setRampRoutingDecision>
170+
| ReturnType<typeof setHasAgreedTransakNativePolicy>;
165171

166172
export type Region = Country & State;
167173

0 commit comments

Comments
 (0)