Skip to content

Commit 1393baa

Browse files
authored
feat(deposit): transactions analytics (#17339)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR adds analytics tracking for Transaction related events. (Confirmed, Completed, Failed) <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] 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** - [ ] 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.
1 parent 66669be commit 1393baa

File tree

7 files changed

+476
-11
lines changed

7 files changed

+476
-11
lines changed

app/components/UI/Ramp/Deposit/Views/BankDetails/BankDetails.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import { FIAT_ORDER_STATES } from '../../../../../../constants/on-ramp';
3535
import { processFiatOrder } from '../../../index';
3636
import { useTheme } from '../../../../../../util/theme';
3737
import { RootState } from '../../../../../../reducers';
38-
import { hasDepositOrderField } from '../../utils';
38+
import {
39+
getCryptoCurrencyFromTransakId,
40+
hasDepositOrderField,
41+
} from '../../utils';
3942
import { useDepositSDK } from '../../sdk';
4043
import Button, {
4144
ButtonSize,
@@ -44,6 +47,7 @@ import Button, {
4447
import { SUPPORTED_PAYMENT_METHODS } from '../../constants';
4548
import { DepositOrder } from '@consensys/native-ramps-sdk';
4649
import PrivacySection from '../../components/PrivacySection';
50+
import useAnalytics from '../../../hooks/useAnalytics';
4751

4852
export interface BankDetailsParams {
4953
orderId: string;
@@ -59,7 +63,8 @@ const BankDetails = () => {
5963
const { colors } = useTheme();
6064
const dispatch = useDispatch();
6165
const dispatchThunk = useThunkDispatch();
62-
const { sdk } = useDepositSDK();
66+
const { sdk, selectedWalletAddress, selectedRegion } = useDepositSDK();
67+
const trackEvent = useAnalytics();
6368

6469
const { orderId, shouldUpdate = true } = useParams<BankDetailsParams>();
6570
const order = useSelector((state: RootState) => getOrderById(state, orderId));
@@ -210,6 +215,25 @@ const BankDetails = () => {
210215
return;
211216
}
212217

218+
const cryptoCurrency = getCryptoCurrencyFromTransakId(
219+
order.data.cryptoCurrency,
220+
);
221+
222+
trackEvent('RAMPS_TRANSACTION_CONFIRMED', {
223+
ramp_type: 'DEPOSIT',
224+
amount_source: Number(order.data.fiatAmount),
225+
amount_destination: Number(order.cryptoAmount),
226+
exchange_rate: Number(order.data.exchangeRate),
227+
gas_fee: 0, //Number(order.data.gasFee),
228+
processing_fee: 0, //Number(order.data.processingFee),
229+
total_fee: Number(order.data.totalFeesFiat),
230+
payment_method_id: order.data.paymentMethod,
231+
country: selectedRegion?.isoCode || '',
232+
chain_id: cryptoCurrency?.chainId || '',
233+
currency_destination: selectedWalletAddress || order.data.walletAddress,
234+
currency_source: order.data.fiatCurrency,
235+
});
236+
213237
await confirmPayment(order.id, paymentOptionId);
214238

215239
await handleOnRefresh();
@@ -222,7 +246,15 @@ const BankDetails = () => {
222246
} catch (fetchError) {
223247
console.error(fetchError);
224248
}
225-
}, [navigation, confirmPayment, handleOnRefresh, order]);
249+
}, [
250+
navigation,
251+
confirmPayment,
252+
handleOnRefresh,
253+
order,
254+
selectedRegion?.isoCode,
255+
selectedWalletAddress,
256+
trackEvent,
257+
]);
226258

227259
const handleCancelOrder = useCallback(async () => {
228260
try {

app/components/UI/Ramp/Deposit/Views/OrderProcessing/OrderProcessing.test.tsx

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const mockSetOptions = jest.fn();
1212
const mockLinkingOpenURL = jest.fn();
1313
const mockUseDepositSDK = jest.fn();
1414
const mockCancelOrder = jest.fn();
15+
const mockTrackEvent = jest.fn();
1516

1617
jest.mock('@react-navigation/native', () => {
1718
const actualNav = jest.requireActual('@react-navigation/native');
@@ -58,6 +59,8 @@ jest.mock('../../hooks/useDepositSdkMethod', () => ({
5859
}),
5960
}));
6061

62+
jest.mock('../../../hooks/useAnalytics', () => () => mockTrackEvent);
63+
6164
describe('OrderProcessing Component', () => {
6265
const mockOrder = {
6366
id: 'test-order-id',
@@ -71,15 +74,37 @@ describe('OrderProcessing Component', () => {
7174
data: {
7275
cryptoCurrency: 'USDC',
7376
providerOrderLink: 'https://transak.com/order/123',
77+
fiatAmount: '100',
78+
exchangeRate: '2000',
79+
totalFeesFiat: '2.50',
80+
networkFees: '2.50',
81+
partnerFees: '2.50',
82+
paymentMethod: 'credit_debit_card',
83+
walletAddress: '0x1234567890123456789012345678901234567890',
84+
fiatCurrency: 'USD',
7485
},
7586
};
7687

88+
const mockSelectedRegion = {
89+
isoCode: 'US',
90+
flag: '🇺🇸',
91+
name: 'United States',
92+
currency: 'USD',
93+
supported: true,
94+
};
95+
96+
const mockSelectedWalletAddress =
97+
'0x1234567890123456789012345678901234567890';
98+
7799
beforeEach(() => {
78100
jest.clearAllMocks();
79101
(getOrderById as jest.Mock).mockReturnValue(mockOrder);
80102
mockUseDepositSDK.mockReturnValue({
81103
isAuthenticated: false,
104+
selectedRegion: mockSelectedRegion,
105+
selectedWalletAddress: mockSelectedWalletAddress,
82106
});
107+
mockTrackEvent.mockClear();
83108
});
84109

85110
it('renders success state correctly', () => {
@@ -180,4 +205,210 @@ describe('OrderProcessing Component', () => {
180205

181206
expect(mockNavigate).toHaveBeenCalledWith(Routes.DEPOSIT.BUILD_QUOTE);
182207
});
208+
209+
describe('Analytics Event Tracking', () => {
210+
describe('RAMPS_TRANSACTION_COMPLETED tracking', () => {
211+
it('tracks RAMPS_TRANSACTION_COMPLETED event when order state is COMPLETED', () => {
212+
renderWithProvider(<OrderProcessing />, {
213+
state: {
214+
engine: {
215+
backgroundState,
216+
},
217+
},
218+
});
219+
220+
expect(mockTrackEvent).toHaveBeenCalledWith(
221+
'RAMPS_TRANSACTION_COMPLETED',
222+
{
223+
ramp_type: 'DEPOSIT',
224+
amount_source: 100,
225+
amount_destination: 0.05,
226+
exchange_rate: 2000,
227+
gas_fee: 2.5,
228+
processing_fee: 2.5,
229+
total_fee: 2.5,
230+
payment_method_id: 'credit_debit_card',
231+
country: 'US',
232+
chain_id: 'eip155:1',
233+
currency_destination: mockSelectedWalletAddress,
234+
currency_source: 'USD',
235+
},
236+
);
237+
});
238+
239+
it('tracks RAMPS_TRANSACTION_COMPLETED with order wallet address when selectedWalletAddress is not available', () => {
240+
mockUseDepositSDK.mockReturnValueOnce({
241+
isAuthenticated: false,
242+
selectedRegion: mockSelectedRegion,
243+
selectedWalletAddress: null,
244+
});
245+
246+
renderWithProvider(<OrderProcessing />, {
247+
state: {
248+
engine: {
249+
backgroundState,
250+
},
251+
},
252+
});
253+
254+
expect(mockTrackEvent).toHaveBeenCalledWith(
255+
'RAMPS_TRANSACTION_COMPLETED',
256+
{
257+
ramp_type: 'DEPOSIT',
258+
amount_source: 100,
259+
amount_destination: 0.05,
260+
exchange_rate: 2000,
261+
gas_fee: 2.5,
262+
processing_fee: 2.5,
263+
total_fee: 2.5,
264+
payment_method_id: 'credit_debit_card',
265+
country: 'US',
266+
chain_id: 'eip155:1',
267+
currency_destination: '0x1234567890123456789012345678901234567890',
268+
currency_source: 'USD',
269+
},
270+
);
271+
});
272+
273+
it('tracks RAMPS_TRANSACTION_COMPLETED with correct number conversions for all numeric fields', () => {
274+
const orderWithStringNumbers = {
275+
...mockOrder,
276+
data: {
277+
...mockOrder.data,
278+
fiatAmount: '250.75',
279+
exchangeRate: '1850.25',
280+
totalFeesFiat: '5.99',
281+
networkFees: '5.99',
282+
partnerFees: '5.99',
283+
},
284+
cryptoAmount: '0.135',
285+
};
286+
(getOrderById as jest.Mock).mockReturnValue(orderWithStringNumbers);
287+
288+
renderWithProvider(<OrderProcessing />, {
289+
state: {
290+
engine: {
291+
backgroundState,
292+
},
293+
},
294+
});
295+
296+
expect(mockTrackEvent).toHaveBeenCalledWith(
297+
'RAMPS_TRANSACTION_COMPLETED',
298+
{
299+
ramp_type: 'DEPOSIT',
300+
amount_source: 250.75,
301+
amount_destination: 0.135,
302+
exchange_rate: 1850.25,
303+
gas_fee: 5.99,
304+
processing_fee: 5.99,
305+
total_fee: 5.99,
306+
payment_method_id: 'credit_debit_card',
307+
country: 'US',
308+
chain_id: 'eip155:1',
309+
currency_destination: mockSelectedWalletAddress,
310+
currency_source: 'USD',
311+
},
312+
);
313+
});
314+
});
315+
316+
describe('RAMPS_TRANSACTION_FAILED tracking', () => {
317+
it('tracks RAMPS_TRANSACTION_FAILED event when order state is FAILED', () => {
318+
const failedOrder = { ...mockOrder, state: FIAT_ORDER_STATES.FAILED };
319+
(getOrderById as jest.Mock).mockReturnValue(failedOrder);
320+
321+
renderWithProvider(<OrderProcessing />, {
322+
state: {
323+
engine: {
324+
backgroundState,
325+
},
326+
},
327+
});
328+
329+
expect(mockTrackEvent).toHaveBeenCalledWith(
330+
'RAMPS_TRANSACTION_FAILED',
331+
{
332+
ramp_type: 'DEPOSIT',
333+
amount_source: 100,
334+
amount_destination: 0.05,
335+
exchange_rate: 2000,
336+
gas_fee: 2.5,
337+
processing_fee: 2.5,
338+
total_fee: 2.5,
339+
payment_method_id: 'credit_debit_card',
340+
country: 'US',
341+
chain_id: 'eip155:1',
342+
currency_destination: mockSelectedWalletAddress,
343+
currency_source: 'USD',
344+
error_message: 'transaction_failed',
345+
},
346+
);
347+
});
348+
});
349+
350+
describe('No analytics tracking scenarios', () => {
351+
it('does not track analytics events for PENDING state', () => {
352+
const pendingOrder = { ...mockOrder, state: FIAT_ORDER_STATES.PENDING };
353+
(getOrderById as jest.Mock).mockReturnValue(pendingOrder);
354+
355+
renderWithProvider(<OrderProcessing />, {
356+
state: {
357+
engine: {
358+
backgroundState,
359+
},
360+
},
361+
});
362+
363+
expect(mockTrackEvent).not.toHaveBeenCalled();
364+
});
365+
366+
it('does not track analytics events for CREATED state', () => {
367+
const createdOrder = { ...mockOrder, state: FIAT_ORDER_STATES.CREATED };
368+
(getOrderById as jest.Mock).mockReturnValue(createdOrder);
369+
370+
renderWithProvider(<OrderProcessing />, {
371+
state: {
372+
engine: {
373+
backgroundState,
374+
},
375+
},
376+
});
377+
378+
expect(mockTrackEvent).not.toHaveBeenCalled();
379+
});
380+
381+
it('does not track analytics events for CANCELLED state', () => {
382+
const cancelledOrder = {
383+
...mockOrder,
384+
state: FIAT_ORDER_STATES.CANCELLED,
385+
};
386+
(getOrderById as jest.Mock).mockReturnValue(cancelledOrder);
387+
388+
renderWithProvider(<OrderProcessing />, {
389+
state: {
390+
engine: {
391+
backgroundState,
392+
},
393+
},
394+
});
395+
396+
expect(mockTrackEvent).not.toHaveBeenCalled();
397+
});
398+
399+
it('does not track analytics events when order is null', () => {
400+
(getOrderById as jest.Mock).mockReturnValue(null);
401+
402+
renderWithProvider(<OrderProcessing />, {
403+
state: {
404+
engine: {
405+
backgroundState,
406+
},
407+
},
408+
});
409+
410+
expect(mockTrackEvent).not.toHaveBeenCalled();
411+
});
412+
});
413+
});
183414
});

0 commit comments

Comments
 (0)