Skip to content

Commit

Permalink
temp+feat: enable STX by default with migration and notification (#12857
Browse files Browse the repository at this point in the history
)

<!--
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]>
  • Loading branch information
runway-github[bot] and metamaskbot authored Feb 14, 2025
1 parent a1d4e5f commit 40ee39f
Show file tree
Hide file tree
Showing 63 changed files with 1,090 additions and 1,374 deletions.
2 changes: 1 addition & 1 deletion .js.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export IS_TEST=""
# but have to be defined here for local tests
export MM_TEST_ACCOUNT_SRP=""
export MM_TEST_ACCOUNT_PRIVATE_KEY=""
export MM_STAKE_TEST_ACCOUNT_PRIVATE_KEY=""
export TENDERLY_NETWORK_ID=""

# address is the address of the first account generated from the previous SRP
export MM_TEST_ACCOUNT_ADDRESS=""
Expand Down
605 changes: 544 additions & 61 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ android {
applicationId "io.metamask"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.39.0"
versionCode 1544
versionName "7.41.0"
versionCode 1557
testBuildType System.getProperty('testBuildType', 'debug')
missingDimensionStrategy 'react-native-camera', 'general'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
exports[`BottomSheetHeader should render snapshot correctly 1`] = `
<View
style={
[
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
},
false,
]
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
}
}
testID="header"
>
Expand Down
34 changes: 0 additions & 34 deletions app/component-library/components/HeaderBase/HeaderBase.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Third party dependencies.
import React from 'react';
import { render } from '@testing-library/react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

// External dependencies.
import Text, { TextVariant } from '../Texts/Text';
Expand All @@ -16,16 +15,7 @@ import {
HEADERBASE_TITLE_TEST_ID,
} from './HeaderBase.constants';

jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn(),
}));
describe('HeaderBase', () => {
const mockInsets = { top: 20, bottom: 0, left: 0, right: 0 };

beforeEach(() => {
(useSafeAreaInsets as jest.Mock).mockReturnValue(mockInsets);
});

it('should render snapshot correctly', () => {
const wrapper = render(<HeaderBase>Sample HeaderBase Title</HeaderBase>);
expect(wrapper).toMatchSnapshot();
Expand Down Expand Up @@ -65,28 +55,4 @@ describe('HeaderBase', () => {

expect(getByRole('text').props.style.fontFamily).toBe(fontFamily);
});

it('applies marginTop when includesTopInset is true', () => {
const { getByTestId } = render(
<HeaderBase includesTopInset>Header Content</HeaderBase>,
);

const headerBase = getByTestId(HEADERBASE_TEST_ID);
// Verify the marginTop is applied
expect(headerBase.props.style).toEqual(
expect.arrayContaining([{ marginTop: mockInsets.top }]),
);
});

it('does not apply marginTop when includesTopInset is false', () => {
const { getByTestId } = render(
<HeaderBase includesTopInset={false}>Header Content</HeaderBase>,
);

const headerBase = getByTestId(HEADERBASE_TEST_ID);
// Verify the marginTop is not applied
expect(headerBase.props.style).toEqual(
expect.not.arrayContaining([{ marginTop: mockInsets.top }]),
);
});
});
9 changes: 1 addition & 8 deletions app/component-library/components/HeaderBase/HeaderBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// Third party dependencies.
import React from 'react';
import { View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

// External dependencies.
import { useComponentSize, useStyles } from '../../hooks';
Expand All @@ -23,25 +22,19 @@ const HeaderBase: React.FC<HeaderBaseProps> = ({
children,
startAccessory,
endAccessory,
includesTopInset = false,
}) => {
const { size: startAccessorySize, onLayout: startAccessoryOnLayout } =
useComponentSize();
const { size: endAccessorySize, onLayout: endAccessoryOnLayout } =
useComponentSize();
const insets = useSafeAreaInsets();

const { styles } = useStyles(styleSheet, {
style,
startAccessorySize,
endAccessorySize,
});

return (
<View
style={[styles.base, includesTopInset && { marginTop: insets.top }]}
testID={HEADERBASE_TEST_ID}
>
<View style={styles.base} testID={HEADERBASE_TEST_ID}>
<View style={styles.accessoryWrapper}>
<View onLayout={startAccessoryOnLayout}>{startAccessory}</View>
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@ export interface HeaderBaseProps extends ViewProps {
* Optional prop to include content to be displayed after the title.
*/
endAccessory?: React.ReactNode;
/**
* Optional prop to include the top inset to make sure the header is visible
* below device's knob
* @default: false
*/
includesTopInset?: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
exports[`HeaderBase should render snapshot correctly 1`] = `
<View
style={
[
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
},
false,
]
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
}
}
testID="header"
>
Expand Down
4 changes: 3 additions & 1 deletion app/components/Base/RemoteImage/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { act, render } from '@testing-library/react-native';

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockImplementation(() => 'https://dweb.link/ipfs/'),
useSelector: jest
.fn()
.mockImplementation(() => 'https://dweb.link/ipfs/'),
}));

jest.mock('../../../components/hooks/useIpfsGateway', () => jest.fn());
Expand Down
89 changes: 52 additions & 37 deletions app/components/UI/Navbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import Icon, {
IconColor,
} from '../../../component-library/components/Icons/Icon';
import { AddContactViewSelectorsIDs } from '../../../../e2e/selectors/Settings/Contacts/AddContactView.selectors';
import HeaderBase from '../../../component-library/components/HeaderBase';
import AddressCopy from '../AddressCopy';
import PickerAccount from '../../../component-library/components/Pickers/PickerAccount';
import { createAccountSelectorNavDetails } from '../../../components/Views/AccountSelector';
Expand Down Expand Up @@ -144,12 +143,6 @@ const styles = StyleSheet.create({
top: 2,
right: 10,
},
headerLeftButton: {
marginHorizontal: 16,
},
headerRightButton: {
marginHorizontal: 16,
},
addressCopyWrapper: {
marginHorizontal: 4,
},
Expand Down Expand Up @@ -1323,40 +1316,62 @@ export function getNetworkNavbarOptions(
contentOffset = 0,
networkName = '',
) {
const innerStyles = StyleSheet.create({
headerStyle: {
backgroundColor: themeColors.background.default,
shadowColor: importedColors.transparent,
elevation: 0,
},
headerShadow: {
elevation: 2,
shadowColor: themeColors.background.primary,
shadowOpacity: contentOffset < 20 ? contentOffset / 100 : 0.2,
shadowOffset: { height: 4, width: 0 },
shadowRadius: 8,
},
headerIcon: {
color: themeColors.primary.default,
},
});
return {
header: () => (
<HeaderBase
includesTopInset
startAccessory={
<ButtonIcon
style={styles.headerLeftButton}
onPress={() => navigation.pop()}
testID={CommonSelectorsIDs.BACK_ARROW_BUTTON}
size={ButtonIconSizes.Lg}
iconName={IconName.ArrowLeft}
iconColor={IconColor.Default}
/>
}
endAccessory={
onRightPress && (
<ButtonIcon
style={styles.headerRightButton}
onPress={onRightPress}
size={ButtonIconSizes.Lg}
iconName={IconName.MoreVertical}
iconColor={IconColor.Default}
/>
)
}
headerTitle: () => (
<NavbarTitle
disableNetwork={disableNetwork}
title={title}
translate={translate}
networkName={networkName}
/>
),
headerLeft: () => (
// eslint-disable-next-line react/jsx-no-bind
<TouchableOpacity
onPress={() => navigation.pop()}
style={styles.backButton}
testID={CommonSelectorsIDs.BACK_ARROW_BUTTON}
>
<NavbarTitle
disableNetwork={disableNetwork}
title={title}
translate={translate}
networkName={networkName}
<IonicIcon
name={'ios-close'}
size={38}
style={innerStyles.headerIcon}
/>
</HeaderBase>
</TouchableOpacity>
),
headerRight: onRightPress
? () => (
<TouchableOpacity style={styles.backButton} onPress={onRightPress}>
<MaterialCommunityIcon
name={'dots-horizontal'}
size={28}
style={innerStyles.headerIcon}
/>
</TouchableOpacity>
// eslint-disable-next-line no-mixed-spaces-and-tabs
)
: () => <View />,
headerStyle: [
innerStyles.headerStyle,
contentOffset && innerStyles.headerShadow,
],
};
}

Expand Down
41 changes: 0 additions & 41 deletions app/components/UI/Navbar/index.test.jsx

This file was deleted.

13 changes: 5 additions & 8 deletions app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,11 @@ exports[`NetworkDetails renders correctly 1`] = `
>
<View
style={
[
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
},
false,
]
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
}
}
testID="header"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ exports[`NetworkVerificationInfo renders correctly 1`] = `
>
<View
style={
[
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
},
false,
]
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
}
}
testID="header"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -953,14 +953,11 @@ exports[`NetworkSwitcher View renders and dismisses network modal when pressing
>
<View
style={
[
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
},
false,
]
{
"backgroundColor": "#ffffff",
"flexDirection": "row",
"padding": 16,
}
}
testID="header"
>
Expand Down
Loading

0 comments on commit 40ee39f

Please sign in to comment.