Skip to content

Commit 6125646

Browse files
Merge branch 'main' of github.com:MetaMask/metamask-mobile into cursor/defi-explore-sites-5eb2
2 parents 3ddcc63 + eedc67b commit 6125646

40 files changed

Lines changed: 1966 additions & 146 deletions

.github/guidelines/E2E_DECISION_TREE.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ To save infra resources while waiting for static analysis findings and potential
3333
- E2E tests are skipped and merge is blocked while the label is present, **unless** all changes are ignorable-only.
3434
- If E2E tests are needed, they should pass to be able to merge.
3535

36-
## AI test selection
36+
## Smart AI E2E test selection
3737

3838
Runs only when all of the following are true:
3939

@@ -53,3 +53,10 @@ Flakiness detection is applied to modified E2E test files in PRs:
5353
- Modified E2E test files run twice
5454
- It applies to existing test files as well as new test files added in the PR
5555
- It can be disabled by adding the label `skip-e2e-flakiness-detection`. Useful when making large refactors or when changes don't pose flakiness risk.
56+
57+
## Release branches
58+
59+
PRs to release branches (cherry-picked from main) are exempt from the following:
60+
61+
- Label `pr-not-ready-for-e2e` is not applied
62+
- Smart AI E2E selection is skipped - all E2E suites are run (if changes are not ignorable-only, e.g. only docs)

app/components/UI/Identity/BackupAndSyncFeaturesToggles/BackupAndSyncFeaturesToggles.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React, { useCallback, useMemo } from 'react';
22
import { View, Switch, InteractionManager } from 'react-native';
33

4-
import Text, {
4+
import {
5+
Text,
56
TextColor,
67
TextVariant,
7-
} from '../../../../component-library/components/Texts/Text';
8+
Icon,
9+
IconName,
10+
} from '@metamask/design-system-react-native';
811
import { useTheme } from '../../../../util/theme';
912
import styles from './BackupAndSyncFeaturesToggles.styles';
1013
import { useBackupAndSync } from '../../../../util/identity/hooks/useBackupAndSync';
@@ -16,9 +19,6 @@ import {
1619
selectIsBackupAndSyncUpdateLoading,
1720
} from '../../../../selectors/identity';
1821
import { BACKUPANDSYNC_FEATURES } from '@metamask/profile-sync-controller/user-storage';
19-
import Icon, {
20-
IconName,
21-
} from '../../../../component-library/components/Icons/Icon';
2222
import { strings } from '../../../../../locales/i18n';
2323
import { MetaMetricsEvents } from '../../../../core/Analytics';
2424
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
@@ -126,10 +126,10 @@ const BackupAndSyncFeaturesToggles = () => {
126126
return (
127127
<View style={styles.setting}>
128128
<View style={styles.heading}>
129-
<Text variant={TextVariant.HeadingSM}>
129+
<Text variant={TextVariant.HeadingSm}>
130130
{strings('backupAndSync.manageWhatYouSync.title')}
131131
</Text>
132-
<Text variant={TextVariant.BodySM} color={TextColor.Alternative}>
132+
<Text variant={TextVariant.BodySm} color={TextColor.TextAlternative}>
133133
{strings('backupAndSync.manageWhatYouSync.description')}
134134
</Text>
135135
</View>

app/components/UI/Identity/BackupAndSyncToggle/BackupAndSyncToggle.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import React, { useCallback, useEffect } from 'react';
33
import { View, Switch, Linking, InteractionManager } from 'react-native';
44
// import { useNavigation } from '@react-navigation/native';
55

6-
import Text, {
6+
import {
7+
Text,
78
TextVariant,
89
TextColor,
9-
} from '../../../../component-library/components/Texts/Text';
10+
} from '@metamask/design-system-react-native';
1011
import { useTheme } from '../../../../util/theme';
1112
// import { strings } from '../../../../../locales/i18n';
1213
import styles from './BackupAndSyncToggle.styles';
@@ -150,7 +151,7 @@ const BackupAndSyncToggle = ({
150151
return (
151152
<View style={styles.setting}>
152153
<View style={styles.heading}>
153-
<Text variant={TextVariant.HeadingSM}>
154+
<Text variant={TextVariant.HeadingSm}>
154155
{strings('backupAndSync.title')}
155156
</Text>
156157
<Switch
@@ -165,9 +166,9 @@ const BackupAndSyncToggle = ({
165166
testID={BACKUP_AND_SYNC_TOGGLE_TEST_IDS.TOGGLE}
166167
/>
167168
</View>
168-
<Text variant={TextVariant.BodyMD} color={TextColor.Alternative}>
169+
<Text variant={TextVariant.BodyMd} color={TextColor.TextAlternative}>
169170
{strings('backupAndSync.enable.description')}
170-
<Text color={TextColor.Info} onPress={handleLink}>
171+
<Text color={TextColor.InfoDefault} onPress={handleLink}>
171172
{strings('backupAndSync.privacyLink')}
172173
</Text>
173174
</Text>

app/components/UI/Identity/ConfirmTurnOnBackupAndSyncModal/ConfirmTurnOnBackupAndSyncModal.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import React, { useRef } from 'react';
22

3-
import BottomSheet, {
4-
BottomSheetRef,
5-
} from '../../../../component-library/components/BottomSheets/BottomSheet';
6-
import { strings } from '../../../../../locales/i18n';
7-
83
import {
4+
BottomSheet,
5+
type BottomSheetRef,
96
IconColor,
107
IconName,
118
IconSize,
12-
} from '../../../../component-library/components/Icons/Icon';
9+
} from '@metamask/design-system-react-native';
10+
import { strings } from '../../../../../locales/i18n';
1311
import ModalContent from '../../Notification/Modal';
1412
import { toggleBasicFunctionality } from '../../../../actions/settings';
1513
import { useParams } from '../../../../util/navigation/navUtils';
@@ -45,7 +43,7 @@ const ConfirmTurnOnBackupAndSyncModal = () => {
4543
const turnContent = {
4644
icon: {
4745
name: IconName.Check,
48-
color: IconColor.Success,
46+
color: IconColor.SuccessDefault,
4947
},
5048
bottomSheetTitle: strings('backupAndSync.enable.title'),
5149
bottomSheetMessage: strings('backupAndSync.enable.confirmation'),

app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { strings } from '../../../../../../locales/i18n';
2323
import MOCK_MONEY_TRANSACTIONS from '../../constants/mockActivityData';
2424
import useMoneyAccountBalance from '../../hooks/useMoneyAccountBalance';
2525
import { selectIsCardholder } from '../../../../../selectors/cardController';
26+
import { getDetectedGeolocation } from '../../../../../reducers/fiatOrders';
2627
import { moneyFormatFiat } from '../../utils/moneyFormatFiat';
2728

2829
const mockGoBack = jest.fn();
@@ -85,7 +86,13 @@ jest.mock('../../../../../selectors/cardController', () => ({
8586
selectIsCardholder: jest.fn(),
8687
}));
8788

89+
jest.mock('../../../../../reducers/fiatOrders', () => ({
90+
...jest.requireActual('../../../../../reducers/fiatOrders'),
91+
getDetectedGeolocation: jest.fn(),
92+
}));
93+
8894
const mockSelectIsCardholder = jest.mocked(selectIsCardholder);
95+
const mockGetDetectedGeolocation = jest.mocked(getDetectedGeolocation);
8996

9097
const mockUseMoneyAccountTransactions = jest.mocked(
9198
useMoneyAccountTransactions,
@@ -134,6 +141,7 @@ describe('MoneyHomeView', () => {
134141
global.alert = jest.fn();
135142

136143
mockSelectIsCardholder.mockReturnValue(false);
144+
mockGetDetectedGeolocation.mockReturnValue('US');
137145

138146
mockUseMoneyAccountBalance.mockReturnValue({
139147
totalFiatFormatted: '$3.00',
@@ -592,4 +600,82 @@ describe('MoneyHomeView', () => {
592600
});
593601
});
594602
});
603+
604+
describe('Metal card geolocation gating', () => {
605+
it('renders the Metal card row when geolocation is US', () => {
606+
mockGetDetectedGeolocation.mockReturnValue('US');
607+
608+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
609+
610+
expect(
611+
getByTestId(MoneyMetaMaskCardTestIds.METAL_CARD_ROW),
612+
).toBeOnTheScreen();
613+
expect(
614+
getByTestId(MoneyMetaMaskCardTestIds.VIRTUAL_CARD_ROW),
615+
).toBeOnTheScreen();
616+
});
617+
618+
it('renders the Metal card row when geolocation is a US sub-region (e.g. US-CA)', () => {
619+
mockGetDetectedGeolocation.mockReturnValue('us-ca');
620+
621+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
622+
623+
expect(
624+
getByTestId(MoneyMetaMaskCardTestIds.METAL_CARD_ROW),
625+
).toBeOnTheScreen();
626+
});
627+
628+
it('hides the Metal card row when geolocation is GB', () => {
629+
mockGetDetectedGeolocation.mockReturnValue('GB');
630+
631+
const { queryByTestId, getByTestId } = renderWithProvider(
632+
<MoneyHomeView />,
633+
);
634+
635+
expect(
636+
queryByTestId(MoneyMetaMaskCardTestIds.METAL_CARD_ROW),
637+
).not.toBeOnTheScreen();
638+
expect(
639+
getByTestId(MoneyMetaMaskCardTestIds.VIRTUAL_CARD_ROW),
640+
).toBeOnTheScreen();
641+
});
642+
643+
it('hides the Metal card row when geolocation is undefined (loading/unknown - fail closed)', () => {
644+
mockGetDetectedGeolocation.mockReturnValue(undefined);
645+
646+
const { queryByTestId, getByTestId } = renderWithProvider(
647+
<MoneyHomeView />,
648+
);
649+
650+
expect(
651+
queryByTestId(MoneyMetaMaskCardTestIds.METAL_CARD_ROW),
652+
).not.toBeOnTheScreen();
653+
expect(
654+
getByTestId(MoneyMetaMaskCardTestIds.VIRTUAL_CARD_ROW),
655+
).toBeOnTheScreen();
656+
});
657+
});
658+
659+
describe('Get now navigation', () => {
660+
it('navigates to the card sign-up flow when the virtual card Get now button is pressed', () => {
661+
mockGetDetectedGeolocation.mockReturnValue('GB');
662+
663+
const { getByText } = renderWithProvider(<MoneyHomeView />);
664+
665+
fireEvent.press(getByText(strings('money.metamask_card.get_now')));
666+
667+
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.ROOT);
668+
});
669+
670+
it('navigates to the card sign-up flow when the metal card Get now button is pressed', () => {
671+
mockGetDetectedGeolocation.mockReturnValue('US');
672+
673+
const { getAllByText } = renderWithProvider(<MoneyHomeView />);
674+
const buttons = getAllByText(strings('money.metamask_card.get_now'));
675+
676+
fireEvent.press(buttons[1]);
677+
678+
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.ROOT);
679+
});
680+
});
595681
});

app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { TokenDetailsSource } from '../../../TokenDetails/constants/constants';
3636
import AppConstants from '../../../../../core/AppConstants';
3737
import NavigationService from '../../../../../core/NavigationService';
3838
import { selectIsCardholder } from '../../../../../selectors/cardController';
39+
import { getDetectedGeolocation } from '../../../../../reducers/fiatOrders';
3940
import Logger from '../../../../../util/Logger';
4041
import { AssetType } from '../../../../Views/confirmations/types/token';
4142
import { Hex } from '@metamask/utils';
@@ -73,6 +74,8 @@ const MoneyHomeView = () => {
7374
const { allTransactions, moneyAddress } = useMoneyAccountTransactions();
7475

7576
const isCardholder = useSelector(selectIsCardholder);
77+
const geolocation = useSelector(getDetectedGeolocation);
78+
const isUS = geolocation?.toUpperCase().split('-')[0] === 'US';
7679

7780
const homeState = getMoneyHomeState(allTransactions.length);
7881
const isMilestone = homeState === 'milestone' || homeState === 'filled';
@@ -296,6 +299,7 @@ const MoneyHomeView = () => {
296299
onHeaderPress={handleHeaderPress}
297300
onLinkPress={handleLinkCardPress}
298301
apy={apyPercent}
302+
showMetalCard={isUS}
299303
/>
300304
<Divider />
301305
{isMilestone && (

app/components/UI/Money/components/MoneyMetaMaskCard/MoneyMetaMaskCard.test.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ describe('MoneyMetaMaskCard', () => {
3232
).toBeOnTheScreen();
3333
});
3434

35-
it('renders metal card row', () => {
35+
it('renders metal card row when showMetalCard is true', () => {
3636
const { getByText, getByTestId } = render(
37-
<MoneyMetaMaskCard onGetNowPress={jest.fn()} />,
37+
<MoneyMetaMaskCard onGetNowPress={jest.fn()} showMetalCard />,
3838
);
3939

4040
expect(
@@ -48,14 +48,36 @@ describe('MoneyMetaMaskCard', () => {
4848
).toBeOnTheScreen();
4949
});
5050

51+
it('hides metal card row by default (showMetalCard not provided)', () => {
52+
const { queryByTestId, queryByText } = render(
53+
<MoneyMetaMaskCard onGetNowPress={jest.fn()} />,
54+
);
55+
56+
expect(
57+
queryByTestId(MoneyMetaMaskCardTestIds.METAL_CARD_ROW),
58+
).not.toBeOnTheScreen();
59+
expect(
60+
queryByText(strings('money.metamask_card.metal_card')),
61+
).not.toBeOnTheScreen();
62+
});
63+
64+
it('hides metal card row when showMetalCard is false', () => {
65+
const { queryByTestId } = render(
66+
<MoneyMetaMaskCard onGetNowPress={jest.fn()} showMetalCard={false} />,
67+
);
68+
69+
expect(
70+
queryByTestId(MoneyMetaMaskCardTestIds.METAL_CARD_ROW),
71+
).not.toBeOnTheScreen();
72+
});
73+
5174
it('calls onGetNowPress when virtual card Get now is pressed', () => {
5275
const mockGetNow = jest.fn();
53-
const { getAllByText } = render(
76+
const { getByText } = render(
5477
<MoneyMetaMaskCard onGetNowPress={mockGetNow} />,
5578
);
56-
const getNowButtons = getAllByText(strings('money.metamask_card.get_now'));
5779

58-
fireEvent.press(getNowButtons[0]);
80+
fireEvent.press(getByText(strings('money.metamask_card.get_now')));
5981

6082
expect(mockGetNow).toHaveBeenCalledTimes(1);
6183
expect(mockGetNow.mock.calls[0]).toEqual([]);
@@ -64,7 +86,7 @@ describe('MoneyMetaMaskCard', () => {
6486
it('calls onGetNowPress when metal card Get now is pressed', () => {
6587
const mockGetNow = jest.fn();
6688
const { getAllByText } = render(
67-
<MoneyMetaMaskCard onGetNowPress={mockGetNow} />,
89+
<MoneyMetaMaskCard onGetNowPress={mockGetNow} showMetalCard />,
6890
);
6991
const getNowButtons = getAllByText(strings('money.metamask_card.get_now'));
7092

@@ -174,9 +196,9 @@ describe('MoneyMetaMaskCard', () => {
174196
});
175197

176198
describe('upsell mode (default)', () => {
177-
it('renders virtual and metal card rows', () => {
199+
it('renders virtual and metal card rows when showMetalCard is true', () => {
178200
const { getByTestId } = render(
179-
<MoneyMetaMaskCard onGetNowPress={jest.fn()} />,
201+
<MoneyMetaMaskCard onGetNowPress={jest.fn()} showMetalCard />,
180202
);
181203

182204
expect(
@@ -187,6 +209,19 @@ describe('MoneyMetaMaskCard', () => {
187209
).toBeOnTheScreen();
188210
});
189211

212+
it('renders only the virtual card row when showMetalCard is false', () => {
213+
const { getByTestId, queryByTestId } = render(
214+
<MoneyMetaMaskCard onGetNowPress={jest.fn()} showMetalCard={false} />,
215+
);
216+
217+
expect(
218+
getByTestId(MoneyMetaMaskCardTestIds.VIRTUAL_CARD_ROW),
219+
).toBeOnTheScreen();
220+
expect(
221+
queryByTestId(MoneyMetaMaskCardTestIds.METAL_CARD_ROW),
222+
).not.toBeOnTheScreen();
223+
});
224+
190225
it('does not render link mode elements', () => {
191226
const { queryByTestId } = render(
192227
<MoneyMetaMaskCard onGetNowPress={jest.fn()} />,

app/components/UI/Money/components/MoneyMetaMaskCard/MoneyMetaMaskCard.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ interface MoneyMetaMaskCardProps {
3434
onLinkPress?: () => void;
3535
/** Current APY value displayed in the link mode bullet. */
3636
apy?: number;
37+
/**
38+
* Whether to render the Metal card row in upsell mode. Defaults to `false`
39+
* because the Metal card is currently only available to US users; the parent
40+
* is expected to pass the geolocation-derived flag.
41+
*/
42+
showMetalCard?: boolean;
3743
}
3844

3945
const CardRow = ({
@@ -166,6 +172,7 @@ const MoneyMetaMaskCard = ({
166172
onHeaderPress,
167173
onLinkPress,
168174
apy,
175+
showMetalCard = false,
169176
}: MoneyMetaMaskCardProps) => {
170177
const handleLinkPress = useCallback(() => onLinkPress?.(), [onLinkPress]);
171178

@@ -200,13 +207,15 @@ const MoneyMetaMaskCard = ({
200207
onPress={onGetNowPress}
201208
testID={MoneyMetaMaskCardTestIds.VIRTUAL_CARD_ROW}
202209
/>
203-
<CardRow
204-
imageSource={mmCardMetal}
205-
cardName={strings('money.metamask_card.metal_card')}
206-
cashbackPercentage="3"
207-
onPress={onGetNowPress}
208-
testID={MoneyMetaMaskCardTestIds.METAL_CARD_ROW}
209-
/>
210+
{showMetalCard && (
211+
<CardRow
212+
imageSource={mmCardMetal}
213+
cardName={strings('money.metamask_card.metal_card')}
214+
cashbackPercentage="3"
215+
onPress={onGetNowPress}
216+
testID={MoneyMetaMaskCardTestIds.METAL_CARD_ROW}
217+
/>
218+
)}
210219
</>
211220
)}
212221
</Box>

0 commit comments

Comments
 (0)