Skip to content

Commit 7e4b5a0

Browse files
authored
Merge branch 'main' into feat/android-16kb-requirement
2 parents 89406a0 + 75d1e2b commit 7e4b5a0

4 files changed

Lines changed: 176 additions & 29 deletions

File tree

.github/scripts/collect-qa-stats.mjs

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
*
2323
* MetaMetrics (top-level `metametrics` namespace): static scan of
2424
* `tests/helpers/analytics/expectations/*.ts` plus `LEGACY_INLINE_METAMETRICS_PATHS`
25-
* for specs not yet using declarative expectations.
25+
* for specs not yet using declarative expectations. Event names are picked from
26+
* `name:` fields, `eventNames: [...]`, `onboardingEvents.*`, and event-ish `const` arrays.
2627
*
2728
* Example output:
2829
* {
@@ -348,7 +349,63 @@ function parseConstStringLiterals(source) {
348349
}
349350

350351
/**
351-
* Event names from declarative `*.analytics.ts` modules (onboarding refs, `name:` entries, event arrays).
352+
* Content between "[" and matching "]" at the same nesting depth (naive bracket count).
353+
*
354+
* @param {string} source
355+
* @param {number} openBracketIdx index of '[' opening the array
356+
* @returns {string|null}
357+
*/
358+
function sliceBalancedSquareBracketInner(source, openBracketIdx) {
359+
if (source[openBracketIdx] !== '[') return null;
360+
let depth = 0;
361+
for (let i = openBracketIdx; i < source.length; i += 1) {
362+
const c = source[i];
363+
if (c === '[') depth += 1;
364+
else if (c === ']') {
365+
depth -= 1;
366+
if (depth === 0) return source.slice(openBracketIdx + 1, i);
367+
}
368+
}
369+
return null;
370+
}
371+
372+
/**
373+
* Segment from CSV inside `eventNames:` or event-ish `const` arrays. Spread/rest is skipped —
374+
* duplicated by a sibling `const *Names*` list when present (e.g. `...transactionEventNames`).
375+
*
376+
* @param {string} token
377+
* @param {Record<string, string>} onboardingMap
378+
* @param {Record<string, string>} strConsts
379+
* @returns {string|null}
380+
*/
381+
function resolveDeclarativeExpectationListToken(token, onboardingMap, strConsts) {
382+
const t = token.replace(/^\s+|\s+$/g, '');
383+
if (!t) return null;
384+
const lit = t.match(/^['"]([^'"]+)['"]$/);
385+
if (lit) return lit[1];
386+
const onb = t.match(/^onboardingEvents\.(\w+)$/);
387+
if (onb && onboardingMap[onb[1]]) return onboardingMap[onb[1]];
388+
if (/^\.\.\.\s*\w+$/.test(t)) return null;
389+
if (strConsts[t]) return strConsts[t];
390+
return null;
391+
}
392+
393+
/**
394+
* @param {string} inner
395+
* @param {Record<string, string>} onboardingMap
396+
* @param {Record<string, string>} strConsts
397+
* @param {Set<string>} out
398+
*/
399+
function collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out) {
400+
for (const part of inner.split(',')) {
401+
const v = resolveDeclarativeExpectationListToken(part, onboardingMap, strConsts);
402+
if (v) out.add(v);
403+
}
404+
}
405+
406+
/**
407+
* Event names from declarative `*.analytics.ts`: `eventNames:` arrays, onboarding refs,
408+
* `name:` entries, string/const lookups, and event-ish `const [...]` declarations.
352409
*
353410
* @param {string} source
354411
* @param {Record<string, string>} onboardingMap
@@ -368,11 +425,18 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
368425
const v = onboardingMap[m[1]];
369426
if (v) out.add(v);
370427
}
371-
for (const m of source.matchAll(/\bname:\s*(\w+)\s*,/g)) {
428+
// Allow `name: IDENT,` (more properties follow) or `name: IDENT }` (single-field expectation object).
429+
for (const m of source.matchAll(/\bname:\s*(\w+)\s*[},]/g)) {
372430
const v = strConsts[m[1]];
373431
if (v) out.add(v);
374432
}
375433

434+
for (const em of source.matchAll(/\beventNames:\s*\[/g)) {
435+
const openIdx = em.index + em[0].length - 1;
436+
const inner = sliceBalancedSquareBracketInner(source, openIdx);
437+
if (inner) collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
438+
}
439+
376440
const reArrays = /\bconst\s+(\w+)\s*=\s*\[([\s\S]*?)\];/g;
377441
let am;
378442
while ((am = reArrays.exec(source)) !== null) {
@@ -382,21 +446,7 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
382446
/(?:event|Event|Expected|expectation|analytics|Names)/.test(varName) ||
383447
/\bonboardingEvents\b|\bexpectedEvents\b/.test(inner);
384448
if (!looksLikeEventList) continue;
385-
for (const part of inner.split(',')) {
386-
const t = part.replace(/^\s+|\s+$/g, '');
387-
if (!t) continue;
388-
const lit = t.match(/^['"]([^'"]+)['"]$/);
389-
if (lit) {
390-
out.add(lit[1]);
391-
continue;
392-
}
393-
const onb = t.match(/^onboardingEvents\.(\w+)$/);
394-
if (onb && onboardingMap[onb[1]]) {
395-
out.add(onboardingMap[onb[1]]);
396-
continue;
397-
}
398-
if (strConsts[t]) out.add(strConsts[t]);
399-
}
449+
collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
400450
}
401451
}
402452

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const ConnectQRHardwareSelectorsIDs = {
2+
CONTAINER: 'connect-qr-hardware-container',
3+
HEADER: 'connect-qr-hardware-header',
24
CONTINUE_BUTTON: 'qr-continue-button',
35
};

app/components/Views/ConnectQRHardware/index.test.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import renderWithProvider from '../../../util/test/renderWithProvider';
33
import Engine from '../../../core/Engine';
44
import ConnectQRHardware from './index';
5+
import { StyleSheet } from 'react-native';
56
import { fireEvent, act, waitFor } from '@testing-library/react-native';
67
import { ConnectQRHardwareSelectorsIDs } from './ConnectQRHardware.testIds';
78
import { backgroundState } from '../../../util/test/initial-root-state';
@@ -37,6 +38,30 @@ jest.mock('../../../components/hooks/useAnalytics/useAnalytics', () => ({
3738
}),
3839
}));
3940

41+
jest.mock('react-native-safe-area-context', () => {
42+
const ReactActual = jest.requireActual('react');
43+
const { View: MockView } = jest.requireActual('react-native');
44+
const inset = { top: 44, right: 0, bottom: 0, left: 0 };
45+
const frame = { x: 0, y: 0, width: 390, height: 844 };
46+
47+
return {
48+
...jest.requireActual('react-native-safe-area-context'),
49+
SafeAreaInsetsContext: ReactActual.createContext(inset),
50+
SafeAreaFrameContext: ReactActual.createContext(frame),
51+
SafeAreaView: ({ children, ...props }: { children: React.ReactNode }) =>
52+
ReactActual.createElement(MockView, props, children),
53+
SafeAreaProvider: ({ children, ...props }: { children: React.ReactNode }) =>
54+
ReactActual.createElement(MockView, props, children),
55+
SafeAreaConsumer: ({
56+
children,
57+
}: {
58+
children: (safeAreaInsets: typeof inset) => React.ReactNode;
59+
}) => children(inset),
60+
useSafeAreaInsets: () => inset,
61+
useSafeAreaFrame: () => frame,
62+
};
63+
});
64+
4065
jest.mock(
4166
'../../../core/HardwareWallet/contexts/HardwareWalletContext',
4267
() => ({
@@ -272,6 +297,70 @@ describe('ConnectQRHardware', () => {
272297
);
273298
});
274299

300+
it('does not add header top margin when SafeAreaView handles top inset', async () => {
301+
mockKeyringController.getAccounts.mockResolvedValue([]);
302+
303+
const { getByTestId } = renderWithProvider(
304+
<ConnectQRHardware navigation={mockedNavigate} />,
305+
{ state: mockInitialState },
306+
);
307+
308+
await waitFor(() => {
309+
expect(mockKeyringController.getAccounts).toHaveBeenCalledTimes(1);
310+
});
311+
312+
const header = getByTestId(ConnectQRHardwareSelectorsIDs.HEADER);
313+
314+
expect(header).toBeOnTheScreen();
315+
expect(StyleSheet.flatten(header.props.style).marginTop).toBeUndefined();
316+
});
317+
318+
it('excludes bottom edge from parent SafeAreaView because instruction owns bottom spacing', async () => {
319+
mockKeyringController.getAccounts.mockResolvedValue([]);
320+
321+
const { getByTestId } = renderWithProvider(
322+
<ConnectQRHardware navigation={mockedNavigate} />,
323+
{ state: mockInitialState },
324+
);
325+
326+
await waitFor(() => {
327+
expect(mockKeyringController.getAccounts).toHaveBeenCalledTimes(1);
328+
});
329+
330+
const safeAreaContainer = getByTestId(
331+
ConnectQRHardwareSelectorsIDs.CONTAINER,
332+
);
333+
334+
expect(safeAreaContainer.props.edges).toStrictEqual([
335+
'top',
336+
'left',
337+
'right',
338+
]);
339+
});
340+
341+
it('excludes bottom edge from parent SafeAreaView when account selector owns bottom spacing', async () => {
342+
mockKeyringController.getAccounts.mockResolvedValue([]);
343+
344+
const { getByTestId } = renderWithProvider(
345+
<ConnectQRHardware navigation={mockedNavigate} />,
346+
{ state: mockInitialState },
347+
);
348+
349+
const button = getByTestId(ConnectQRHardwareSelectorsIDs.CONTINUE_BUTTON);
350+
351+
await act(async () => {
352+
fireEvent.press(button);
353+
});
354+
355+
await waitFor(() => {
356+
expect(mockQrKeyring.getFirstPage).toHaveBeenCalledTimes(1);
357+
});
358+
359+
expect(
360+
getByTestId(ConnectQRHardwareSelectorsIDs.CONTAINER).props.edges,
361+
).toStrictEqual(['top', 'left', 'right']);
362+
});
363+
275364
it('renders first page correctly when user clicks `continue` button', async () => {
276365
mockKeyringController.getAccounts.mockResolvedValue([]);
277366

app/components/Views/ConnectQRHardware/index.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import React, {
66
useState,
77
} from 'react';
88
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
9-
import {
10-
type EdgeInsets,
11-
useSafeAreaInsets,
12-
} from 'react-native-safe-area-context';
9+
import { SafeAreaView, type Edge } from 'react-native-safe-area-context';
1310
import Engine from '../../../core/Engine';
1411
import AnimatedQRScannerModal from '../../UI/QRHardware/AnimatedQRScanner';
1512
import AccountSelector from '../../UI/HardwareWallet/AccountSelector';
@@ -35,6 +32,7 @@ import { QrScanRequestType } from '@metamask/eth-qr-keyring';
3532
import { withQrKeyring } from '../../../core/QrKeyring/QrKeyring';
3633
import { getChecksumAddress } from '@metamask/utils';
3734
import { getConnectedDevicesCount } from '../../../core/HardwareWallets/analytics';
35+
import { ConnectQRHardwareSelectorsIDs } from './ConnectQRHardware.testIds';
3836
import { useHardwareWallet } from '../../../core/HardwareWallet/contexts/HardwareWalletContext';
3937
import { HardwareWalletType } from '@metamask/hw-wallet-sdk';
4038
import { useQrScanErrorForwarding } from '../../../core/HardwareWallet/hooks/useQrScanErrorForwarding';
@@ -48,15 +46,16 @@ interface IConnectQRHardwareProps {
4846
route?: any;
4947
}
5048

51-
const createStyles = (colors: ThemeColors, insets: EdgeInsets) =>
49+
const SAFE_AREA_EDGES: Edge[] = ['top', 'left', 'right'];
50+
51+
const createStyles = (colors: ThemeColors) =>
5252
StyleSheet.create({
5353
container: {
5454
flex: 1,
5555
flexDirection: 'column',
5656
alignItems: 'center',
5757
},
5858
header: {
59-
marginTop: insets.top,
6059
flexDirection: 'row',
6160
width: '100%',
6261
paddingHorizontal: 32,
@@ -94,8 +93,7 @@ const ConnectQRHardware = ({ navigation, route }: IConnectQRHardwareProps) => {
9493
const { colors } = useTheme();
9594
const { trackEvent, createEventBuilder } = useAnalytics();
9695
const { setTargetWalletType, setQrScanRetryHandler } = useHardwareWallet();
97-
const insets = useSafeAreaInsets();
98-
const styles = createStyles(colors, insets);
96+
const styles = createStyles(colors);
9997
const hideMarketingContent = route?.params?.hideMarketingContent ?? false;
10098

10199
const [isScanning, setIsScanning] = useState(false);
@@ -112,6 +110,7 @@ const ConnectQRHardware = ({ navigation, route }: IConnectQRHardwareProps) => {
112110
}, []);
113111

114112
const [existingAccounts, setExistingAccounts] = useState<string[]>([]);
113+
const safeAreaEdges = SAFE_AREA_EDGES;
115114

116115
useEffect(() => {
117116
setTargetWalletType(HardwareWalletType.Qr);
@@ -313,8 +312,15 @@ const ConnectQRHardware = ({ navigation, route }: IConnectQRHardwareProps) => {
313312

314313
return (
315314
<Fragment>
316-
<View style={styles.container}>
317-
<View style={styles.header}>
315+
<SafeAreaView
316+
style={styles.container}
317+
edges={safeAreaEdges}
318+
testID={ConnectQRHardwareSelectorsIDs.CONTAINER}
319+
>
320+
<View
321+
style={styles.header}
322+
testID={ConnectQRHardwareSelectorsIDs.HEADER}
323+
>
318324
<Icon
319325
name="qrcode"
320326
size={42}
@@ -347,7 +353,7 @@ const ConnectQRHardware = ({ navigation, route }: IConnectQRHardwareProps) => {
347353
title={strings('connect_qr_hardware.select_accounts')}
348354
/>
349355
)}
350-
</View>
356+
</SafeAreaView>
351357
<AnimatedQRScannerModal
352358
visible={isScanning}
353359
purpose={QrScanRequestType.PAIR}

0 commit comments

Comments
 (0)