Skip to content

Commit 40ee39f

Browse files
temp+feat: enable STX by default with migration and notification (#12857)
<!-- 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 enables Smart Transactions (STX) in MetaMask Mobile by default through migration number #68 for users who have either opted out with notification if the Smart Transactions toggle has been enabled for them. Docs: [SmartTransactionsMigrationBanner](https://www.notion.so/consensys/SmartTransactionsMigrationBanner-Feature-Documentation-180fc61a326e80a19da2ce55e6c8687f) How it works (if user does not have STX enabled or prior STX Transaction history: - Upon Migration STX is enabled in Settings: - A banner alert will displays on the following transaction confirmations - Send Confirmation flow - Swaps confirmation flow - Contract deployment & interactions (deploy, minting, etc.) In the case a user migrates from a previous version of the Mobile app and the migration runs and sets STX toggle "ON" in `Settings > Advanced > Smart Transactions`, they will receive an Alert on transaction confirmation screens until dismissed, or by clicking on the "Higher success rates" link within the alert. If they click on the link in the banner alert they will get sent to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) for more information. When returning to the confirmation they just navigated from the banner alert should not show and should never show again. Edge Cases: If a user is new and setting up a wallet for the first time, they will not receive the Banner Alert. If a user imports a new wallet during a fresh install of the extension on a new browser or recovers a wallet, it's possible they may not see the alert if STX was on in a previous install. The STX Banner Alert is dismissed and will not show again if a user is in the state to get shown the banner and toggles STX off independently even if they do not physically dismiss the STX Banner Alert. Migration Logic: 1. If `smartTransactionsOptInStatus` is `null` (new/never interacted) - Sets status to true - Enables notification flag 2. If status is false (previously opted out): - With no Ethereum Mainnet STX activity: Sets to true with notification - With existing Mainnet STX activity: Preserves user preference 3. If status is true: No changes needed UI Components: Implements `SmartTransactionsMigrationBanner` component for user notification. The notification system bridges the migration changes with the UI, ensuring users are informed of the STX enablement while maintaining their ability to opt out through settings. Target release: release-7.39.0 Affected user base: ~5.7M users who previously opted out of STX but have no STX activity. ## **Running Unit Tests** Migration 067 test: ```bash yarn jest "./app/store/migrations/068.test.ts" --no-cache ``` SmartTransactions Migration Banner component test: ```bash yarn jest "./app/components/Views/confirmations/components/SmartTransactionsMigrationBanner/SmartTransactionsMigrationBanner.test.ts" --no-cache ``` QuotesView component test: ```bash yarn jest "./app/components/UI/Swaps/QuotesView.test.ts" --no-cache ``` SendFlow component test: ```bash yarn jest "./app/components/Views/confirmations/SendFlow/Confirm/index.test.tsx" --no-cache ``` TransactionReview component test: ```bash yarn jest "./app/components/Views/confirmations/components/TransactionReview/index.test.tsx" --no-cach ``` ## **Manual testing steps** **Test Migration (using a wallet/account with no STX Transactions)** Start with an older repo version: **Terminal #1** ```bash git checkout tags/v7.32.0 ``` ```bash yarn setup && yarn watch ``` **Terminal #2** ```bash yarn start:ios ``` 1. Import or setup a wallet without STX transactions, launch the wallet (do not enable STX if prompted), check that toggle is OFF in: `Settings > Advanced > Smart Transactions` 2. Switch to feature branch and run app: **Terminal #1** ```bash git checkout feat/enable-stx-migration ``` ```bash yarn setup && yarn watch ``` **Terminal #2** ```bash yarn start:ios ``` 3. Test that Alert only shows during confirmation screens for transactions and contract interactions, but not for signing 4. Create a Send transaction to your own wallet for `0.0001` ETH 5. Ensure that Smart Transactions Banner Alert IS showing 6. Start a Swaps transaction on Ethereum Mainnet 7 Ensure that Smart Transactions Banner Alert IS showing 8. Try several Signs (ETH Sign, Personal Sign, Sign Typed Data, etc..) and ensure the STX Banner Alert does not show on those confirmations screens. 9. Ensure that the Smart Transactions Banner Alert is only showing on STX supported chains: - Ethereum - Sepolia 10. Ensure that link in alert "Higher success rates" link (inspect) goes to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) 11. Dismiss the Smart Transactions Banner Alert 12. Create a Send transaction to your own wallet for `0.0001` ETH 13. Ensure that Smart Transactions Banner Alert IS NOT showing * Note: The Smart Transactions Banner Alert should not show up on chains like Linea which are not supported ## **Screenshots/Recordings** The following before and after screenshots show the state of varying Mobile views like Swaps, Send, Contract Deployment, Signing, etc.. that show the `SmartTransactionsMigrationBanner` component and how it's displayed on each view. The before shows the view without the component being rendered and the after (after migration and before dismissal) show the component as it will appear for the user on each view. ### **Before** <img width="150" alt="01-stx_before" src="https://github.com/user-attachments/assets/f526d21a-8a38-40ed-810e-1c102636ccbb" /> <img width="150" alt="02-send_before" src="https://github.com/user-attachments/assets/78155c14-295d-4d8a-94b6-6845158b29bf" /> <img width="150" alt="02-sendLegacy_before" src="https://github.com/user-attachments/assets/8f0049c5-ab00-45d0-adae-ed7d203ec79d" /> <img width="150" alt="04-signTypedDataV4_before" src="https://github.com/user-attachments/assets/07eef175-a431-4c88-a752-db8ea16c3328" /> <img width="150" alt="06-contractInteraction_before" src="https://github.com/user-attachments/assets/dcb63432-9f2d-4589-b6b6-25d5e13ea28f" /> <img width="150" alt="05-contractDeployment_before" src="https://github.com/user-attachments/assets/61dbc3f6-599b-435b-a994-07b1e1f41bcf" /> ### **After** <img width="150" alt="01-stx_after" src="https://github.com/user-attachments/assets/dae49867-093d-475f-a16c-911e79406e3f" /> <img width="150" alt="02-send_after" src="https://github.com/user-attachments/assets/5152d738-304d-4fa8-a835-84c4ba22878b" /> <img width="150" alt="02-sendLegacy_after" src="https://github.com/user-attachments/assets/84eebe4f-f374-48de-9f4a-76ab33da64f2" /> <img width="150" alt="04-signTypedDataV4_after" src="https://github.com/user-attachments/assets/c99b0251-2a9b-4f3a-83c3-a3ab5d6523ac" /> <img width="150" alt="06-contractInteraction_after" src="https://github.com/user-attachments/assets/09731034-a4cc-40f1-b5ff-23b2eccee0d5" /> <img width="150" alt="05-contractDeployment_after" src="https://github.com/user-attachments/assets/3c0ab0ba-2188-45e3-b0ae-622800f76773" /> <img width="150" alt="09-swap_after" src="https://github.com/user-attachments/assets/0943676d-eb21-4aa1-a975-3545d029bf34" /> <img width="150" alt="09-swap_after" src="https://github.com/user-attachments/assets/1b50c741-3105-4fb0-b010-ba08111ead89" /> ## **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. ## **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. --------- Co-authored-by: metamaskbot <[email protected]>
1 parent a1d4e5f commit 40ee39f

File tree

63 files changed

+1090
-1374
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1090
-1374
lines changed

.js.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export IS_TEST=""
3333
# but have to be defined here for local tests
3434
export MM_TEST_ACCOUNT_SRP=""
3535
export MM_TEST_ACCOUNT_PRIVATE_KEY=""
36-
export MM_STAKE_TEST_ACCOUNT_PRIVATE_KEY=""
36+
export TENDERLY_NETWORK_ID=""
3737

3838
# address is the address of the first account generated from the previous SRP
3939
export MM_TEST_ACCOUNT_ADDRESS=""

CHANGELOG.md

Lines changed: 544 additions & 61 deletions
Large diffs are not rendered by default.

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ android {
178178
applicationId "io.metamask"
179179
minSdkVersion rootProject.ext.minSdkVersion
180180
targetSdkVersion rootProject.ext.targetSdkVersion
181-
versionName "7.39.0"
182-
versionCode 1544
181+
versionName "7.41.0"
182+
versionCode 1557
183183
testBuildType System.getProperty('testBuildType', 'debug')
184184
missingDimensionStrategy 'react-native-camera', 'general'
185185
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33
exports[`BottomSheetHeader should render snapshot correctly 1`] = `
44
<View
55
style={
6-
[
7-
{
8-
"backgroundColor": "#ffffff",
9-
"flexDirection": "row",
10-
"padding": 16,
11-
},
12-
false,
13-
]
6+
{
7+
"backgroundColor": "#ffffff",
8+
"flexDirection": "row",
9+
"padding": 16,
10+
}
1411
}
1512
testID="header"
1613
>

app/component-library/components/HeaderBase/HeaderBase.test.tsx

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Third party dependencies.
22
import React from 'react';
33
import { render } from '@testing-library/react-native';
4-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
54

65
// External dependencies.
76
import Text, { TextVariant } from '../Texts/Text';
@@ -16,16 +15,7 @@ import {
1615
HEADERBASE_TITLE_TEST_ID,
1716
} from './HeaderBase.constants';
1817

19-
jest.mock('react-native-safe-area-context', () => ({
20-
useSafeAreaInsets: jest.fn(),
21-
}));
2218
describe('HeaderBase', () => {
23-
const mockInsets = { top: 20, bottom: 0, left: 0, right: 0 };
24-
25-
beforeEach(() => {
26-
(useSafeAreaInsets as jest.Mock).mockReturnValue(mockInsets);
27-
});
28-
2919
it('should render snapshot correctly', () => {
3020
const wrapper = render(<HeaderBase>Sample HeaderBase Title</HeaderBase>);
3121
expect(wrapper).toMatchSnapshot();
@@ -65,28 +55,4 @@ describe('HeaderBase', () => {
6555

6656
expect(getByRole('text').props.style.fontFamily).toBe(fontFamily);
6757
});
68-
69-
it('applies marginTop when includesTopInset is true', () => {
70-
const { getByTestId } = render(
71-
<HeaderBase includesTopInset>Header Content</HeaderBase>,
72-
);
73-
74-
const headerBase = getByTestId(HEADERBASE_TEST_ID);
75-
// Verify the marginTop is applied
76-
expect(headerBase.props.style).toEqual(
77-
expect.arrayContaining([{ marginTop: mockInsets.top }]),
78-
);
79-
});
80-
81-
it('does not apply marginTop when includesTopInset is false', () => {
82-
const { getByTestId } = render(
83-
<HeaderBase includesTopInset={false}>Header Content</HeaderBase>,
84-
);
85-
86-
const headerBase = getByTestId(HEADERBASE_TEST_ID);
87-
// Verify the marginTop is not applied
88-
expect(headerBase.props.style).toEqual(
89-
expect.not.arrayContaining([{ marginTop: mockInsets.top }]),
90-
);
91-
});
9258
});

app/component-library/components/HeaderBase/HeaderBase.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// Third party dependencies.
44
import React from 'react';
55
import { View } from 'react-native';
6-
import { useSafeAreaInsets } from 'react-native-safe-area-context';
76

87
// External dependencies.
98
import { useComponentSize, useStyles } from '../../hooks';
@@ -23,25 +22,19 @@ const HeaderBase: React.FC<HeaderBaseProps> = ({
2322
children,
2423
startAccessory,
2524
endAccessory,
26-
includesTopInset = false,
2725
}) => {
2826
const { size: startAccessorySize, onLayout: startAccessoryOnLayout } =
2927
useComponentSize();
3028
const { size: endAccessorySize, onLayout: endAccessoryOnLayout } =
3129
useComponentSize();
32-
const insets = useSafeAreaInsets();
33-
3430
const { styles } = useStyles(styleSheet, {
3531
style,
3632
startAccessorySize,
3733
endAccessorySize,
3834
});
3935

4036
return (
41-
<View
42-
style={[styles.base, includesTopInset && { marginTop: insets.top }]}
43-
testID={HEADERBASE_TEST_ID}
44-
>
37+
<View style={styles.base} testID={HEADERBASE_TEST_ID}>
4538
<View style={styles.accessoryWrapper}>
4639
<View onLayout={startAccessoryOnLayout}>{startAccessory}</View>
4740
</View>

app/component-library/components/HeaderBase/HeaderBase.types.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ export interface HeaderBaseProps extends ViewProps {
1717
* Optional prop to include content to be displayed after the title.
1818
*/
1919
endAccessory?: React.ReactNode;
20-
/**
21-
* Optional prop to include the top inset to make sure the header is visible
22-
* below device's knob
23-
* @default: false
24-
*/
25-
includesTopInset?: boolean;
2620
}
2721

2822
/**

app/component-library/components/HeaderBase/__snapshots__/HeaderBase.test.tsx.snap

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
exports[`HeaderBase should render snapshot correctly 1`] = `
44
<View
55
style={
6-
[
7-
{
8-
"backgroundColor": "#ffffff",
9-
"flexDirection": "row",
10-
},
11-
false,
12-
]
6+
{
7+
"backgroundColor": "#ffffff",
8+
"flexDirection": "row",
9+
}
1310
}
1411
testID="header"
1512
>

app/components/Base/RemoteImage/index.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { act, render } from '@testing-library/react-native';
66

77
jest.mock('react-redux', () => ({
88
...jest.requireActual('react-redux'),
9-
useSelector: jest.fn().mockImplementation(() => 'https://dweb.link/ipfs/'),
9+
useSelector: jest
10+
.fn()
11+
.mockImplementation(() => 'https://dweb.link/ipfs/'),
1012
}));
1113

1214
jest.mock('../../../components/hooks/useIpfsGateway', () => jest.fn());

app/components/UI/Navbar/index.js

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ import Icon, {
5353
IconColor,
5454
} from '../../../component-library/components/Icons/Icon';
5555
import { AddContactViewSelectorsIDs } from '../../../../e2e/selectors/Settings/Contacts/AddContactView.selectors';
56-
import HeaderBase from '../../../component-library/components/HeaderBase';
5756
import AddressCopy from '../AddressCopy';
5857
import PickerAccount from '../../../component-library/components/Pickers/PickerAccount';
5958
import { createAccountSelectorNavDetails } from '../../../components/Views/AccountSelector';
@@ -144,12 +143,6 @@ const styles = StyleSheet.create({
144143
top: 2,
145144
right: 10,
146145
},
147-
headerLeftButton: {
148-
marginHorizontal: 16,
149-
},
150-
headerRightButton: {
151-
marginHorizontal: 16,
152-
},
153146
addressCopyWrapper: {
154147
marginHorizontal: 4,
155148
},
@@ -1323,40 +1316,62 @@ export function getNetworkNavbarOptions(
13231316
contentOffset = 0,
13241317
networkName = '',
13251318
) {
1319+
const innerStyles = StyleSheet.create({
1320+
headerStyle: {
1321+
backgroundColor: themeColors.background.default,
1322+
shadowColor: importedColors.transparent,
1323+
elevation: 0,
1324+
},
1325+
headerShadow: {
1326+
elevation: 2,
1327+
shadowColor: themeColors.background.primary,
1328+
shadowOpacity: contentOffset < 20 ? contentOffset / 100 : 0.2,
1329+
shadowOffset: { height: 4, width: 0 },
1330+
shadowRadius: 8,
1331+
},
1332+
headerIcon: {
1333+
color: themeColors.primary.default,
1334+
},
1335+
});
13261336
return {
1327-
header: () => (
1328-
<HeaderBase
1329-
includesTopInset
1330-
startAccessory={
1331-
<ButtonIcon
1332-
style={styles.headerLeftButton}
1333-
onPress={() => navigation.pop()}
1334-
testID={CommonSelectorsIDs.BACK_ARROW_BUTTON}
1335-
size={ButtonIconSizes.Lg}
1336-
iconName={IconName.ArrowLeft}
1337-
iconColor={IconColor.Default}
1338-
/>
1339-
}
1340-
endAccessory={
1341-
onRightPress && (
1342-
<ButtonIcon
1343-
style={styles.headerRightButton}
1344-
onPress={onRightPress}
1345-
size={ButtonIconSizes.Lg}
1346-
iconName={IconName.MoreVertical}
1347-
iconColor={IconColor.Default}
1348-
/>
1349-
)
1350-
}
1337+
headerTitle: () => (
1338+
<NavbarTitle
1339+
disableNetwork={disableNetwork}
1340+
title={title}
1341+
translate={translate}
1342+
networkName={networkName}
1343+
/>
1344+
),
1345+
headerLeft: () => (
1346+
// eslint-disable-next-line react/jsx-no-bind
1347+
<TouchableOpacity
1348+
onPress={() => navigation.pop()}
1349+
style={styles.backButton}
1350+
testID={CommonSelectorsIDs.BACK_ARROW_BUTTON}
13511351
>
1352-
<NavbarTitle
1353-
disableNetwork={disableNetwork}
1354-
title={title}
1355-
translate={translate}
1356-
networkName={networkName}
1352+
<IonicIcon
1353+
name={'ios-close'}
1354+
size={38}
1355+
style={innerStyles.headerIcon}
13571356
/>
1358-
</HeaderBase>
1357+
</TouchableOpacity>
13591358
),
1359+
headerRight: onRightPress
1360+
? () => (
1361+
<TouchableOpacity style={styles.backButton} onPress={onRightPress}>
1362+
<MaterialCommunityIcon
1363+
name={'dots-horizontal'}
1364+
size={28}
1365+
style={innerStyles.headerIcon}
1366+
/>
1367+
</TouchableOpacity>
1368+
// eslint-disable-next-line no-mixed-spaces-and-tabs
1369+
)
1370+
: () => <View />,
1371+
headerStyle: [
1372+
innerStyles.headerStyle,
1373+
contentOffset && innerStyles.headerShadow,
1374+
],
13601375
};
13611376
}
13621377

0 commit comments

Comments
 (0)