diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2b52cd4f7b0..6a4fe56efba 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,14 +32,6 @@ jobs:
echo "Working tree dirty at end of job"
exit 1
fi
- pr-linter:
- runs-on: ubuntu-latest
- steps:
- # v5.2.0
- - name: Conventional Commit Validation PR Title
- uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
scripts:
runs-on: ubuntu-20.04
needs: setup
@@ -77,6 +69,11 @@ jobs:
- run: yarn test:unit --forceExit --silent
env:
NODE_OPTIONS: --max_old_space_size=20480
+ - name: SonarCloud Scan
+ # v1.9.1
+ uses: SonarSource/sonarcloud-github-action@5875562561d22a34be0c657405578705a169af6c
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Require clean working directory
shell: bash
run: |
@@ -84,12 +81,6 @@ jobs:
echo "Working tree dirty at end of job"
exit 1
fi
- # Enable when team has access
- # call-sonar-workflow:
- # needs: tests
- # uses: ./.github/workflows/sonar.yml
- # secrets:
- # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
check-workflows:
name: Check workflows
runs-on: ubuntu-latest
@@ -105,6 +96,6 @@ jobs:
all-jobs-pass:
name: All jobs pass
runs-on: ubuntu-20.04
- needs: [setup, scripts, tests, check-workflows, pr-linter]
+ needs: [setup, scripts, tests, check-workflows]
steps:
- run: echo "Great success!"
diff --git a/.github/workflows/pr-title-linter.yml b/.github/workflows/pr-title-linter.yml
new file mode 100644
index 00000000000..230ae583e91
--- /dev/null
+++ b/.github/workflows/pr-title-linter.yml
@@ -0,0 +1,15 @@
+name: "Conventional Commit Validation PR Title"
+on:
+ pull_request:
+ branches:
+ - main
+ types: [opened,edited]
+
+jobs:
+ pr-title-linter:
+ runs-on: ubuntu-latest
+ steps:
+ # v5.2.0
+ - uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/remove_labels_after_pr_closed.yml b/.github/workflows/remove_labels_after_pr_closed.yml
new file mode 100644
index 00000000000..68d8921c576
--- /dev/null
+++ b/.github/workflows/remove_labels_after_pr_closed.yml
@@ -0,0 +1,41 @@
+name: Remove labels after issue (or PR) closed
+
+on:
+ issues:
+ types: [closed]
+ pull_request:
+ types: [closed]
+
+jobs:
+ cleanup:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Remove labels
+ env:
+ REPO: ${{ github.repository }}
+ ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ run: |
+ LABELS=(
+ "product-backlog"
+ "needs-design"
+ "design-in-progress"
+ "ready-for-dev"
+ "sprint-backlog"
+ "in-progress"
+ "blocked"
+ "needs-dev-review"
+ "needs-qa"
+ "issues-found"
+ "ready-for-release"
+ )
+
+ for LABEL in "${LABELS[@]}"; do
+ curl \
+ -X DELETE \
+ -H "Authorization: token $GITHUB_TOKEN" \
+ -H "Accept: application/vnd.github.v3+json" \
+ "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels/$LABEL"
+ done
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
deleted file mode 100644
index 9ad35b09349..00000000000
--- a/.github/workflows/sonar.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-name: Sonar
-on:
- workflow_call:
- secrets:
- SONAR_TOKEN:
- required: true
-jobs:
- sonarcloud:
- name: SonarCloud
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis
- - name: SonarCloud Scan
- # v1.9.1
- uses: SonarSource/sonarcloud-github-action@5875562561d22a34be0c657405578705a169af6c
- with:
- args: >
- -Dsonar.javascript.lcov.reportPaths=tests/coverage/lcov.info
- env:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
\ No newline at end of file
diff --git a/.github/workflows/stale-issue-pr.yml b/.github/workflows/stale-issue-pr.yml
index 6a518482067..c21f33a654d 100644
--- a/.github/workflows/stale-issue-pr.yml
+++ b/.github/workflows/stale-issue-pr.yml
@@ -14,6 +14,7 @@ jobs:
with:
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 7 days. Thank you for your contributions.'
stale-issue-label: 'stale'
+ only-issue-labels: 'type-bug'
exempt-issue-labels: 'type-security, type-pinned, feature-request, awaiting-metamask'
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 7 days. Thank you for your contributions.'
stale-pr-label: 'stale'
@@ -24,4 +25,5 @@ jobs:
days-before-pr-stale: 90
days-before-issue-close: 7
days-before-pr-close: 7
+ operations-per-run: 200
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b657fd1158..fd75cfb1435 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,7 @@
- [#6228](https://github.com/MetaMask/metamask-mobile/pull/6228): [UPDATE] Checkbox component
- [#6226](https://github.com/MetaMask/metamask-mobile/pull/6226): [UPDATE] Button's icon props and button org
+
## 6.5.0 - May 4, 2023
- [#5743](https://github.com/MetaMask/metamask-mobile/pull/5743): [FEATURE] On-ramp: Add buy-crypto deeplink
- [#6201](https://github.com/MetaMask/metamask-mobile/pull/6201): [FIX] [SDK] Missing redirect breaking backward compatibility
diff --git a/Gemfile.lock b/Gemfile.lock
index 98d0bfa7800..af2467ee31f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -71,7 +71,7 @@ GEM
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.7)
- rexml (3.2.4)
+ rexml (3.2.5)
ruby-macho (2.5.1)
typhoeus (1.4.0)
ethon (>= 0.9.0)
@@ -87,6 +87,7 @@ GEM
PLATFORMS
arm64-darwin-22
+ x86_64-linux
DEPENDENCIES
cocoapods (>= 1.11.3)
diff --git a/app/actions/modals/index.js b/app/actions/modals/index.js
index e4522617650..c3c3f06681d 100644
--- a/app/actions/modals/index.js
+++ b/app/actions/modals/index.js
@@ -26,13 +26,6 @@ export function toggleDappTransactionModal(show) {
};
}
-export function toggleApproveModal(show) {
- return {
- type: 'TOGGLE_APPROVE_MODAL',
- show,
- };
-}
-
export function toggleInfoNetworkModal(show) {
return {
type: 'TOGGLE_INFO_NETWORK_MODAL',
diff --git a/app/actions/transaction/index.js b/app/actions/transaction/index.js
index c5ca921056d..26dc08621d1 100644
--- a/app/actions/transaction/index.js
+++ b/app/actions/transaction/index.js
@@ -85,36 +85,6 @@ export function prepareTransaction(transaction) {
};
}
-/**
- * Sets transaction object to be sent. All properties can be updated
- *
- * @param {object} config
- * @param {object} config.transaction - Transaction object with from, to, data, gas, gasPrice, value
- * @param {string} config.ensRecipient - Resolved ens name to send the transaction to
- * @param {string} config.transactionToName - Resolved address book name for to address
- * @param {string} config.transactionFromName - Resolved address book name for from address
- * @param {object} config.selectedAsset - Asset to start the transaction with
- * @param {string} config.assetType - The selectedAsset's type
- */
-export function prepareFullTransaction({
- transaction,
- ensRecipient,
- transactionToName,
- transactionFromName,
- selectedAsset,
- assetType,
-}) {
- return {
- type: 'PREPARE_FULL_TRANSACTION',
- transaction,
- ensRecipient,
- transactionToName,
- transactionFromName,
- selectedAsset,
- assetType,
- };
-}
-
/**
* Sets any attribute in transaction object
*
diff --git a/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx b/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx
index 97f21b38438..55e59a63224 100644
--- a/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx
+++ b/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx
@@ -1,9 +1,10 @@
import React from 'react';
-import { AccountBalanceProps } from './AccountBalance.types';
+
import Card from '../../../components/Cards/Card';
import AccountBase from '../AccountBase/AccountBase';
import { ACCOUNT_BALANCE_TEST_ID } from './AccountBalance.constants';
import styles from './AccountBalance.styles';
+import { AccountBalanceProps } from './AccountBalance.types';
const AccountBalance = ({
accountBalance,
diff --git a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx
index 7073e02dbce..9b8daa114fa 100644
--- a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx
+++ b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx
@@ -1,16 +1,17 @@
import React from 'react';
import { View } from 'react-native';
-import { AccountBaseProps } from './AccountBase.types';
-import Text, { TextVariant } from '../../../components/Texts/Text';
-import BadgeWrapper from '../../../components/Badges/BadgeWrapper';
+
import Badge from '../../../../component-library/components/Badges/Badge';
import Avatar, { AvatarVariants } from '../../../components/Avatars/Avatar';
import { AvatarAccountType } from '../../../components/Avatars/Avatar/variants/AvatarAccount';
+import BadgeWrapper from '../../../components/Badges/BadgeWrapper';
+import Text, { TextVariant } from '../../../components/Texts/Text';
import {
ACCOUNT_BALANCE_AVATAR_TEST_ID,
ACCOUNT_BASE_TEST_ID,
} from './AccountBase.constants';
import styles from './AccountBase.styles';
+import { AccountBaseProps } from './AccountBase.types';
const AccountBase = ({
accountBalance,
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts
index 05c7a0d961e..237d3db4b58 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts
+++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts
@@ -20,6 +20,9 @@ const styleSheet = (params: { theme: Theme }) => {
flexDirection: 'row',
justifyContent: 'space-between',
},
+ fixedPadding: {
+ padding: 0,
+ },
body: {
flexDirection: 'row',
flex: 1,
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx
index bf9258c235c..d5548f0386b 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx
+++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx
@@ -1,10 +1,9 @@
+import { shallow } from 'enzyme';
// Third party dependencies.
import React from 'react';
-import { shallow } from 'enzyme';
// External dependencies.
import { TICKER } from '../CustomSpendCap.constants';
-
// Internal dependencies.
import CustomInput from './CustomInput';
import { CustomInputProps } from './CustomInput.types';
@@ -16,9 +15,9 @@ describe('CustomInput', () => {
props = {
ticker: TICKER,
value: '123',
- inputDisabled: true,
+ isInputGreaterThanBalance: false,
+ isEditDisabled: false,
setMaxSelected: jest.fn(),
- defaultValueSelected: true,
setValue: jest.fn(),
};
});
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx
index 32259ac5420..3283f32a7ff 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx
+++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx
@@ -2,24 +2,23 @@
import React from 'react';
import { TextInput, View } from 'react-native';
-// External dependencies.
-import { useStyles } from '../../../hooks';
-import formatNumber from '../../../../util/formatNumber';
import { strings } from '../../../../../locales/i18n';
+import formatNumber from '../../../../util/formatNumber';
import Text, { TextVariant } from '../../../components/Texts/Text';
-
-// Internal dependencies.
-import { CustomInputProps } from './CustomInput.types';
+// External dependencies.
+import { useStyles } from '../../../hooks';
import CUSTOM_INPUT_TEST_ID from './CustomInput.constants';
import stylesheet from './CustomInput.styles';
+// Internal dependencies.
+import { CustomInputProps } from './CustomInput.types';
const CustomInput = ({
ticker,
value,
- inputDisabled,
setMaxSelected,
- defaultValueSelected,
+ isInputGreaterThanBalance,
setValue,
+ isEditDisabled,
}: CustomInputProps) => {
const handleUpdate = (text: string) => {
setValue(text);
@@ -29,7 +28,10 @@ const CustomInput = ({
setMaxSelected(true);
};
- const { styles } = useStyles(stylesheet, {});
+ const {
+ styles,
+ theme: { colors },
+ } = useStyles(stylesheet, {});
const onChangeValueText = (text: string) => {
handleUpdate(text);
@@ -37,25 +39,39 @@ const CustomInput = ({
};
return (
-
+
- {inputDisabled ? (
+ {!isEditDisabled ? (
- ) : defaultValueSelected ? (
+ ) : (
{`${formatNumber(value)} ${ticker}`}
- ) : null}
+ )}
- {inputDisabled && (
+ {!isEditDisabled && (
void;
/**
- * Boolean to determine if the input is disabled
+ * Boolean to determine if input is greater than balance
* @default false
*/
- inputDisabled?: boolean;
- /**
- * Boolean to determine if default value is selected
- * @default false
- */
- defaultValueSelected: boolean;
+ isInputGreaterThanBalance: boolean;
/**
* Function to update max state
*/
setMaxSelected: (value: boolean) => void;
+ /**
+ * Boolean to disable edit
+ */
+ isEditDisabled: boolean;
}
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap b/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap
index 4c8d6c2ddff..e39d127775b 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap
+++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap
@@ -3,13 +3,16 @@
exports[`CustomInput should render correctly 1`] = `
@@ -26,20 +29,23 @@ exports[`CustomInput should render correctly 1`] = `
keyboardType="numeric"
multiline={true}
onChangeText={[Function]}
- placeholder="Enter a number here (DAI)"
+ placeholder="Enter a number here"
style={
- Object {
- "color": "#24272A",
- "flexGrow": 1,
- "fontFamily": "Euclid Circular B",
- "fontSize": 14,
- "fontWeight": "400",
- "letterSpacing": 0,
- "lineHeight": 22,
- "marginRight": 16,
- "paddingBottom": 0,
- "paddingTop": 0,
- }
+ Array [
+ Object {
+ "color": "#24272A",
+ "flexGrow": 1,
+ "fontFamily": "Euclid Circular B",
+ "fontSize": 14,
+ "fontWeight": "400",
+ "letterSpacing": 0,
+ "lineHeight": 22,
+ "marginRight": 16,
+ "paddingBottom": 0,
+ "paddingTop": 0,
+ },
+ false,
+ ]
}
value="123"
/>
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx
index 8f8c4f1aa48..3f8b4d962c5 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx
+++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx
@@ -1,16 +1,17 @@
// Third party dependencies.
import React from 'react';
+
import { storiesOf } from '@storybook/react-native';
+import CustomSpendCap from './CustomSpendCap';
// Internal dependencies.
import {
- TICKER,
ACCOUNT_BALANCE,
- DAPP_PROPOSED_VALUE,
DAPP_DOMAIN,
+ DAPP_PROPOSED_VALUE,
INPUT_VALUE_CHANGED,
+ TICKER,
} from './CustomSpendCap.constants';
-import CustomSpendCap from './CustomSpendCap';
storiesOf('Component Library / CustomSpendCap', module).add('Default', () => (
(
dappProposedValue={DAPP_PROPOSED_VALUE}
domain={DAPP_DOMAIN}
onInputChanged={INPUT_VALUE_CHANGED}
+ isEditDisabled={false}
+ editValue={() => undefined}
+ tokenSpendValue={''}
/>
));
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx
index a2eecfecf16..0a564f89879 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx
+++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx
@@ -1,32 +1,106 @@
+import { shallow } from 'enzyme';
// Third party dependencies.
import React from 'react';
-import { shallow } from 'enzyme';
-// Internal dependencies.
-import { CustomSpendCapProps } from './CustomSpendCap.types';
+import renderWithProvider from '../../../util/test/renderWithProvider';
import CustomSpendCap from './CustomSpendCap';
import {
- TICKER,
ACCOUNT_BALANCE,
- DAPP_PROPOSED_VALUE,
+ CUSTOM_SPEND_CAP_TEST_ID,
DAPP_DOMAIN,
+ DAPP_PROPOSED_VALUE,
INPUT_VALUE_CHANGED,
+ TICKER,
} from './CustomSpendCap.constants';
+// Internal dependencies.
+import { CustomSpendCapProps } from './CustomSpendCap.types';
+
+function RenderCustomSpendCap(
+ tokenSpendValue: string,
+ isInputValid: () => boolean = () => true,
+) {
+ return (
+ ({})}
+ tokenSpendValue={tokenSpendValue}
+ isInputValid={isInputValid}
+ />
+ );
+}
+
+const isInputValid = jest.fn();
describe('CustomSpendCap', () => {
it('should render CustomSpendCap', () => {
- const wrapper = shallow(
- ,
- );
+ const wrapper = shallow(RenderCustomSpendCap(''));
const singleSelectComponent = wrapper.findWhere(
- (node) => node.prop('testID') === 'custom-spend-cap',
+ (node) => node.prop('testID') === CUSTOM_SPEND_CAP_TEST_ID,
);
expect(singleSelectComponent.exists()).toBe(true);
});
+
+ it('should match snapshot', () => {
+ const container = renderWithProvider(RenderCustomSpendCap(''));
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should render error message is value is not a number', async () => {
+ const notANumber = 'abc';
+ const { findByText } = renderWithProvider(RenderCustomSpendCap(notANumber));
+
+ expect(await findByText('Error: Enter only numbers')).toBeDefined();
+ });
+
+ it('should render valid message if value is 0', async () => {
+ const zeroValue = '0';
+ const { findByText } = renderWithProvider(RenderCustomSpendCap(zeroValue));
+
+ expect(
+ await findByText(
+ `Only enter a number that you're comfortable with ${DAPP_DOMAIN} accessing now or in the future. You can always increase the token limit later.`,
+ ),
+ ).toBeDefined();
+ });
+
+ it('should render valid message if value is less than or equal to account balance', async () => {
+ const valueLessThanBalance = '100';
+ const { toJSON } = renderWithProvider(
+ RenderCustomSpendCap(valueLessThanBalance),
+ );
+
+ expect(JSON.stringify(toJSON())).toMatch(
+ `${valueLessThanBalance} ${TICKER}`,
+ );
+ });
+
+ it('should render valid message if value is greater than account balance', async () => {
+ const valueGreaterThanBalance = '300';
+ const valueDifference =
+ Number(valueGreaterThanBalance) - Number(ACCOUNT_BALANCE);
+ const { toJSON } = renderWithProvider(
+ RenderCustomSpendCap(valueGreaterThanBalance),
+ );
+
+ expect(JSON.stringify(toJSON())).toMatch(`${valueDifference} ${TICKER}`);
+ });
+
+ it('should call isInputValid with false if value is not a number', async () => {
+ const notANumber = 'abc';
+ renderWithProvider(RenderCustomSpendCap(notANumber, isInputValid));
+
+ expect(isInputValid).toHaveBeenCalledWith(false);
+ });
+
+ it('should call isInputValid with true if value is a number', async () => {
+ const validNumber = '100';
+ renderWithProvider(RenderCustomSpendCap(validNumber, isInputValid));
+
+ expect(isInputValid).toHaveBeenCalledWith(true);
+ });
});
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx
index e2412868b0c..a49675af402 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx
+++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx
@@ -1,23 +1,22 @@
+import BigNumber from 'bignumber.js';
// Third party dependencies.
-import React, { useState, useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { Pressable, View } from 'react-native';
-import BigNumber from 'bignumber.js';
-// External dependencies.
-import { useStyles } from '../../hooks';
import { strings } from '../../../../locales/i18n';
-import Button, { ButtonVariants } from '../../components/Buttons/Button';
-import Text, { TextVariant } from '../../components/Texts/Text';
+import InfoModal from '../../../components/UI/Swaps/components/InfoModal';
import formatNumber from '../../../util/formatNumber';
import { isNumber } from '../../../util/number';
+import Button, { ButtonVariants } from '../../components/Buttons/Button';
+import Icon, { IconName, IconSize } from '../../components/Icons/Icon';
+import Text, { TextVariant } from '../../components/Texts/Text';
+// External dependencies.
+import { useStyles } from '../../hooks';
import CustomInput from './CustomInput';
-import InfoModal from '../../../components/UI/Swaps/components/InfoModal';
-
// Internal dependencies.
import { CUSTOM_SPEND_CAP_TEST_ID } from './CustomSpendCap.constants';
-import { CustomSpendCapProps } from './CustomSpendCap.types';
import customSpendCapStyles from './CustomSpendCap.styles';
-import Icon, { IconName, IconSize } from '../../components/Icons/Icon';
+import { CustomSpendCapProps } from './CustomSpendCap.types';
const CustomSpendCap = ({
ticker,
@@ -25,16 +24,19 @@ const CustomSpendCap = ({
accountBalance,
domain,
onInputChanged,
+ isEditDisabled,
+ editValue,
+ tokenSpendValue,
+ isInputValid,
}: CustomSpendCapProps) => {
const {
styles,
theme: { colors },
} = useStyles(customSpendCapStyles, {});
- const [value, setValue] = useState('');
+ const [value, setValue] = useState(tokenSpendValue);
const [inputDisabled, setInputDisabled] = useState(true);
const [maxSelected, setMaxSelected] = useState(false);
- const [defaultValueSelected, setDefaultValueSelected] = useState(false);
const [
inputValueHigherThanAccountBalance,
setInputValueHigherThanAccountBalance,
@@ -45,26 +47,34 @@ const CustomSpendCap = ({
useEffect(() => {
if (isNumber(value)) {
setInputHasError(false);
- return onInputChanged(value);
+ } else {
+ setInputHasError(true);
}
- return setInputHasError(true);
+
+ onInputChanged(value);
}, [value, onInputChanged]);
- const handlePress = () => {
+ useEffect(() => {
+ isInputValid(!inputHasError);
+ }, [inputHasError, isInputValid]);
+
+ const handleDefaultValue = () => {
setMaxSelected(false);
setValue(dappProposedValue);
- setDefaultValueSelected(!defaultValueSelected);
setInputDisabled(!inputDisabled);
};
+ const handlePress = () => {
+ if (isEditDisabled) editValue();
+ handleDefaultValue();
+ };
+
useEffect(() => {
if (maxSelected) setValue(accountBalance);
}, [maxSelected, accountBalance]);
- const editedDefaultValue = new BigNumber(dappProposedValue);
const newValue = new BigNumber(value);
- const dappValue = editedDefaultValue.minus(accountBalance).toFixed();
const difference = newValue.minus(accountBalance).toFixed();
useEffect(() => {
@@ -88,7 +98,7 @@ const CustomSpendCap = ({
{ domain },
);
- const DAPP_PROPOSED_VALUE_GREATER_THAN_ACCOUNT_BALANCE = (
+ const INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE = (
<>
{strings('contract_allowance.custom_spend_cap.this_contract_allows')}
@@ -96,23 +106,19 @@ const CustomSpendCap = ({
{strings('contract_allowance.custom_spend_cap.from_your_current_balance')}
- {` ${formatNumber(dappValue)} ${ticker} `}
+ {` ${formatNumber(difference)} ${ticker} `}
{strings('contract_allowance.custom_spend_cap.future_tokens')}
>
);
- const INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE = (
+ const INPUT_VALUE_LOWER_THAN_ACCOUNT_BALANCE = (
<>
{strings('contract_allowance.custom_spend_cap.this_contract_allows')}
- {` ${formatNumber(accountBalance)} ${ticker} `}
-
- {strings('contract_allowance.custom_spend_cap.from_your_current_balance')}
-
- {` ${formatNumber(difference)} ${ticker} `}
+ {` ${formatNumber(tokenSpendValue ?? '0')} ${ticker} `}
- {strings('contract_allowance.custom_spend_cap.future_tokens')}
+ {strings('contract_allowance.custom_spend_cap.from_your_balance')}
>
);
@@ -120,7 +126,7 @@ const CustomSpendCap = ({
setIsModalVisible(!isModalVisible);
};
- const infoModalTitle = defaultValueSelected ? (
+ const infoModalTitle = inputValueHigherThanAccountBalance ? (
<>
);
+ let message;
+
+ if (!value || !Number(value)) {
+ message = NO_SELECTED;
+ } else if (maxSelected) {
+ message = MAX_VALUE_SELECTED;
+ } else if (inputValueHigherThanAccountBalance) {
+ message = INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE;
+ } else {
+ message = INPUT_VALUE_LOWER_THAN_ACCOUNT_BALANCE;
+ }
+
return (
{isModalVisible ? (
@@ -145,7 +163,7 @@ const CustomSpendCap = ({
title={infoModalTitle}
body={
- {defaultValueSelected
+ {inputValueHigherThanAccountBalance
? strings(
'contract_allowance.custom_spend_cap.info_modal_description_default',
)
@@ -168,9 +186,15 @@ const CustomSpendCap = ({
@@ -180,7 +204,7 @@ const CustomSpendCap = ({
onPress={handlePress}
textVariant={TextVariant.BodyMD}
label={
- defaultValueSelected
+ isEditDisabled
? strings('contract_allowance.custom_spend_cap.edit')
: strings('contract_allowance.custom_spend_cap.use_default')
}
@@ -190,10 +214,10 @@ const CustomSpendCap = ({
{value.length > 0 && inputHasError && (
@@ -201,17 +225,13 @@ const CustomSpendCap = ({
{strings('contract_allowance.custom_spend_cap.error_enter_number')}
)}
-
-
- {defaultValueSelected
- ? DAPP_PROPOSED_VALUE_GREATER_THAN_ACCOUNT_BALANCE
- : maxSelected
- ? MAX_VALUE_SELECTED
- : inputValueHigherThanAccountBalance
- ? INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE
- : NO_SELECTED}
-
-
+ {!isEditDisabled && (
+
+
+ {message}
+
+
+ )}
);
};
diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts
index 0f94d92c2e3..8b4172c9ba1 100644
--- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts
+++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts
@@ -7,4 +7,21 @@ export interface CustomSpendCapProps {
* @param value - The value of the input field
*/
onInputChanged: (value: string) => void;
+ /**
+ * isEditDisabled - Boolean to disable edit
+ * @default false
+ */
+ isEditDisabled: boolean;
+ /**
+ * function to return to input field
+ */
+ editValue: () => void;
+ /**
+ * token spend value - The value of the input field
+ */
+ tokenSpendValue: string;
+ /**
+ * isInputValid - function to check if input is valid and has no errors
+ */
+ isInputValid: (value: boolean) => boolean;
}
diff --git a/app/component-library/components-temp/CustomSpendCap/__snapshots__/CustomSpendCap.test.tsx.snap b/app/component-library/components-temp/CustomSpendCap/__snapshots__/CustomSpendCap.test.tsx.snap
new file mode 100644
index 00000000000..7b6419829be
--- /dev/null
+++ b/app/component-library/components-temp/CustomSpendCap/__snapshots__/CustomSpendCap.test.tsx.snap
@@ -0,0 +1,186 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CustomSpendCap should match snapshot 1`] = `
+
+
+
+
+ Custom spending cap
+
+
+
+
+
+
+ Use default
+
+
+
+
+
+
+
+
+ Max
+
+
+
+
+
+ Only enter a number that you're comfortable with Uniswap.org accessing now or in the future. You can always increase the token limit later.
+
+
+
+`;
diff --git a/app/component-library/components/Banners/Banner/Banner.test.tsx b/app/component-library/components/Banners/Banner/Banner.test.tsx
new file mode 100644
index 00000000000..94ba52874eb
--- /dev/null
+++ b/app/component-library/components/Banners/Banner/Banner.test.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+
+import { render } from '@testing-library/react-native';
+import Banner from './Banner';
+import Text from '../../Texts/Text/Text';
+import { BannerAlertSeverity } from './variants/BannerAlert/BannerAlert.types';
+import { BannerVariant } from './Banner.types';
+import { ButtonVariants } from '../../Buttons/Button';
+import { IconName } from '../../Icons/Icon';
+import { ButtonIconSizes, ButtonIconVariants } from '../../Buttons/ButtonIcon';
+import { TESTID_BANNER_CLOSE_BUTTON_ICON } from './foundation/BannerBase/BannerBase.constants';
+
+describe('Banner', () => {
+ it('should render correctly', () => {
+ const wrapper = render(
+ ,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render correctly with a start accessory', async () => {
+ const wrapper = render(
+ Test Start accessory}
+ />,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ expect(await wrapper.findByText('Test Start accessory')).toBeDefined();
+ });
+
+ it('should render correctly with an action button', async () => {
+ const wrapper = render(
+ jest.fn(),
+ variant: ButtonVariants.Secondary,
+ }}
+ />,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ expect(await wrapper.findByText('Test Action Button')).toBeDefined();
+ });
+
+ it('should render correctly with a close button', async () => {
+ const wrapper = render(
+ jest.fn(),
+ variant: ButtonVariants.Secondary,
+ }}
+ closeButtonProps={{
+ onPress: () => jest.fn(),
+ iconName: IconName.Close,
+ variant: ButtonIconVariants.Primary,
+ size: ButtonIconSizes.Sm,
+ }}
+ />,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ expect(await wrapper.findByText('Test Action Button')).toBeDefined();
+ expect(
+ await wrapper.queryByTestId(TESTID_BANNER_CLOSE_BUTTON_ICON),
+ ).toBeDefined();
+ });
+});
diff --git a/app/component-library/components/Banners/Banner/README.md b/app/component-library/components/Banners/Banner/README.md
index a2e2beeeeab..a8e5ce7e2dd 100644
--- a/app/component-library/components/Banners/Banner/README.md
+++ b/app/component-library/components/Banners/Banner/README.md
@@ -114,6 +114,14 @@ Optional prop to control the close button's props.
| :-------------------------------------------------- | :------------------------------------------------------ |
| [ButtonIconProps](../../../../Buttons/ButtonIcon/ButtonIcon.types.ts) | No |
+### `children`
+
+Optional prop to add children components to the Banner
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| React.ReactNode | No |
+
## Usage
```javascript
diff --git a/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap b/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap
new file mode 100644
index 00000000000..11e7d17f6e3
--- /dev/null
+++ b/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap
@@ -0,0 +1,412 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Banner should render correctly 1`] = `
+
+
+
+
+
+
+ Hello Error Banner World
+
+
+ This is nothing but a test of the emergency broadcast system.
+
+
+
+`;
+
+exports[`Banner should render correctly with a close button 1`] = `
+
+
+
+
+
+
+ Hello Error Banner World
+
+
+ This is nothing but a test of the emergency broadcast system.
+
+
+
+ Test Action Button
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Banner should render correctly with a start accessory 1`] = `
+
+
+
+ Test Start accessory
+
+
+
+
+ Hello Error Banner World
+
+
+ This is nothing but a test of the emergency broadcast system.
+
+
+
+`;
+
+exports[`Banner should render correctly with an action button 1`] = `
+
+
+
+
+
+
+ Hello Error Banner World
+
+
+ This is nothing but a test of the emergency broadcast system.
+
+
+
+ Test Action Button
+
+
+
+
+`;
diff --git a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx
index bf13358249d..b1ae89b3eaa 100644
--- a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx
+++ b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx
@@ -17,6 +17,9 @@ import { SAMPLE_ICON_PROPS } from '../../../../Icons/Icon/Icon.constants';
// Internal dependencies.
import { BannerBaseProps } from './BannerBase.types';
+// Test IDs
+export const TESTID_BANNER_CLOSE_BUTTON_ICON = 'banner-close-button-icon';
+
// Defaults
export const DEFAULT_BANNERBASE_TITLE_TEXTVARIANT = TextVariant.BodyLGMedium;
export const DEFAULT_BANNERBASE_DESCRIPTION_TEXTVARIANT = TextVariant.BodyMD;
diff --git a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx
index 3e2954266b9..ea46b42f796 100644
--- a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx
+++ b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx
@@ -22,6 +22,7 @@ import {
DEFAULT_BANNERBASE_CLOSEBUTTON_BUTTONICONVARIANT,
DEFAULT_BANNERBASE_CLOSEBUTTON_BUTTONICONSIZE,
DEFAULT_BANNERBASE_CLOSEBUTTON_ICONNAME,
+ TESTID_BANNER_CLOSE_BUTTON_ICON,
} from './BannerBase.constants';
const BannerBase: React.FC = ({
@@ -32,6 +33,7 @@ const BannerBase: React.FC = ({
actionButtonProps,
onClose,
closeButtonProps,
+ children,
...props
}) => {
const { styles } = useStyles(styleSheet, { style });
@@ -63,10 +65,12 @@ const BannerBase: React.FC = ({
{...actionButtonProps}
/>
)}
+ {children}
{(onClose || closeButtonProps) && (
{
+ console.log('Cancel button clicked');
+ },
+ },
+ {
+ variant: ButtonVariants.Primary,
+ label: 'Submit',
+ onPress: () => {
+ console.log('Submit button clicked');
+ },
+ },
+ ],
+};
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx
new file mode 100644
index 00000000000..d3548deb6ae
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx
@@ -0,0 +1,41 @@
+/* eslint-disable no-console */
+
+// Third party dependencies.
+import React from 'react';
+import { storiesOf } from '@storybook/react-native';
+import { select } from '@storybook/addon-knobs';
+
+// External dependencies.
+import { storybookPropsGroupID } from '../../../constants/storybook.constants';
+
+// Internal dependencies.
+import BottomSheetFooter from './BottomSheetFooter';
+import {
+ BottomSheetFooterProps,
+ ButtonsAlignment,
+} from './BottomSheetFooter.types';
+import {
+ DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT,
+ SAMPLE_BOTTOMSHEETFOOTER_PROPS,
+} from './BottomSheetFooter.constants';
+
+export const getBottomSheetFooterStoryProps = (): BottomSheetFooterProps => ({
+ buttonsAlignment: select(
+ 'buttonsAlignment',
+ ButtonsAlignment,
+ DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT,
+ storybookPropsGroupID,
+ ),
+ buttonPropsArray: SAMPLE_BOTTOMSHEETFOOTER_PROPS.buttonPropsArray,
+});
+
+const BottomSheetFooterStory = () => (
+
+);
+
+storiesOf('Component Library / BottomSheets', module).add(
+ 'BottomSheetFooter',
+ BottomSheetFooterStory,
+);
+
+export default BottomSheetFooterStory;
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.styles.ts b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.styles.ts
new file mode 100644
index 00000000000..6754be11ffe
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.styles.ts
@@ -0,0 +1,54 @@
+// Third party dependencies.
+import { StyleSheet, ViewStyle } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../../util/theme/models';
+
+// Internal dependencies.
+import {
+ ButtonsAlignment,
+ BottomSheetFooterStyleSheetVars,
+} from './BottomSheetFooter.types';
+
+/**
+ * Style sheet function for BottomSheetFooter component.
+ *
+ * @param params Style sheet params.
+ * @param params.theme App theme from ThemeContext.
+ * @param params.vars Inputs that the style sheet depends on.
+ * @returns StyleSheet object.
+ */
+const styleSheet = (params: {
+ theme: Theme;
+ vars: BottomSheetFooterStyleSheetVars;
+}) => {
+ const { vars, theme } = params;
+ const { style, buttonsAlignment } = vars;
+ const buttonStyle: ViewStyle =
+ buttonsAlignment === ButtonsAlignment.Horizontal
+ ? { flex: 1 }
+ : { alignSelf: 'stretch' };
+
+ return StyleSheet.create({
+ base: Object.assign(
+ {
+ backgroundColor: theme.colors.background.default,
+ flexDirection:
+ buttonsAlignment === ButtonsAlignment.Horizontal ? 'row' : 'column',
+ paddingVertical: 4,
+ paddingHorizontal: 8,
+ } as ViewStyle,
+ style,
+ ) as ViewStyle,
+ button: {
+ ...buttonStyle,
+ },
+ subsequentButton: {
+ ...buttonStyle,
+ marginLeft: buttonsAlignment === ButtonsAlignment.Horizontal ? 16 : 0,
+ marginTop: buttonsAlignment === ButtonsAlignment.Vertical ? 16 : 0,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.test.tsx b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.test.tsx
new file mode 100644
index 00000000000..ed1d3a3b37f
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.test.tsx
@@ -0,0 +1,57 @@
+// Third party dependencies.
+import React from 'react';
+import { render } from '@testing-library/react-native';
+
+// Internal dependencies.
+import BottomSheetFooter from './BottomSheetFooter';
+import {
+ SAMPLE_BOTTOMSHEETFOOTER_PROPS,
+ TESTID_BOTTOMSHEETFOOTER,
+ TESTID_BOTTOMSHEETFOOTER_BUTTON,
+ TESTID_BOTTOMSHEETFOOTER_BUTTON_SUBSEQUENT,
+} from './BottomSheetFooter.constants';
+import { ButtonsAlignment } from './BottomSheetFooter.types';
+
+describe('BottomSheetFooter', () => {
+ it('should render snapshot correctly', () => {
+ const wrapper = render(
+ ,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render the correct default buttonsAlignment', () => {
+ const { getByTestId } = render(
+ ,
+ );
+ expect(
+ getByTestId(TESTID_BOTTOMSHEETFOOTER).props.style.flexDirection,
+ ).toBe('row');
+ });
+
+ it('should render the correct given buttonsAlignment', () => {
+ const givenButtonsAlignment = ButtonsAlignment.Vertical;
+ const { getByTestId } = render(
+ ,
+ );
+ expect(
+ getByTestId(TESTID_BOTTOMSHEETFOOTER).props.style.flexDirection,
+ ).toBe('column');
+ });
+
+ it('should render the correct gap between buttons', () => {
+ const { getByTestId } = render(
+ ,
+ );
+ expect(
+ getByTestId(TESTID_BOTTOMSHEETFOOTER_BUTTON_SUBSEQUENT).props.style
+ .marginLeft,
+ ).toBe(16);
+ expect(
+ getByTestId(TESTID_BOTTOMSHEETFOOTER_BUTTON).props.style.marginLeft,
+ ).not.toBe(16);
+ });
+});
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.tsx b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.tsx
new file mode 100644
index 00000000000..6848ae54800
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.tsx
@@ -0,0 +1,46 @@
+/* eslint-disable react/prop-types */
+
+// Third party dependencies.
+import React from 'react';
+import { View } from 'react-native';
+
+// External dependencies.
+import { useStyles } from '../../../hooks';
+import Button from '../../Buttons/Button';
+
+// Internal dependencies.
+import styleSheet from './BottomSheetFooter.styles';
+import { BottomSheetFooterProps } from './BottomSheetFooter.types';
+import {
+ DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT,
+ TESTID_BOTTOMSHEETFOOTER,
+ TESTID_BOTTOMSHEETFOOTER_BUTTON,
+ TESTID_BOTTOMSHEETFOOTER_BUTTON_SUBSEQUENT,
+} from './BottomSheetFooter.constants';
+
+const BottomSheetFooter: React.FC = ({
+ style,
+ buttonsAlignment = DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT,
+ buttonPropsArray,
+}) => {
+ const { styles } = useStyles(styleSheet, { style, buttonsAlignment });
+
+ return (
+
+ {buttonPropsArray.map((buttonProp, index) => (
+
+ );
+};
+
+export default BottomSheetFooter;
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.types.ts b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.types.ts
new file mode 100644
index 00000000000..71191b31f7e
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.types.ts
@@ -0,0 +1,36 @@
+// Third party dependencies.
+import { ViewProps } from 'react-native';
+
+// External Dependencies.
+import { ButtonProps } from '../../Buttons/Button/Button.types';
+
+/**
+ * Buttons Alignment options.
+ */
+export enum ButtonsAlignment {
+ Horizontal = 'Horizontal',
+ Vertical = 'Vertical',
+}
+
+/**
+ * BottomSheetFooter component props.
+ */
+export interface BottomSheetFooterProps extends ViewProps {
+ /**
+ * Optional prop to control the alignment of the buttons.
+ * @default ButtonsAlignment.Horizontal
+ */
+ buttonsAlignment?: ButtonsAlignment;
+ /**
+ * Array of buttons that will be displayed in the footer
+ */
+ buttonPropsArray: ButtonProps[];
+}
+
+/**
+ * Style sheet input parameters.
+ */
+export type BottomSheetFooterStyleSheetVars = Pick<
+ BottomSheetFooterProps,
+ 'style' | 'buttonsAlignment'
+>;
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/README.md b/app/component-library/components/BottomSheets/BottomSheetFooter/README.md
new file mode 100644
index 00000000000..52663c89edd
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/README.md
@@ -0,0 +1,24 @@
+# BottomSheetFooter
+
+BottomSheetFooter is a Footer component specifically used for BottomSheets.
+
+## Props
+
+This component extends React Native's [ViewProps](https://reactnative.dev/docs/view) component.
+
+### `buttonsAlignment`
+
+Optional prop to control the alignment of the buttons.
+
+| TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| ButtonsAlignment | No | ButtonsAlignment.Horizontal |
+
+
+## Usage
+
+```javascript
+;
+```
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/__snapshots__/BottomSheetFooter.test.tsx.snap b/app/component-library/components/BottomSheets/BottomSheetFooter/__snapshots__/BottomSheetFooter.test.tsx.snap
new file mode 100644
index 00000000000..7a3882cea3d
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/__snapshots__/BottomSheetFooter.test.tsx.snap
@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BottomSheetFooter should render snapshot correctly 1`] = `
+
+
+
+ Cancel
+
+
+
+
+ Submit
+
+
+
+`;
diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/index.ts b/app/component-library/components/BottomSheets/BottomSheetFooter/index.ts
new file mode 100644
index 00000000000..4645d18a0ac
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetFooter/index.ts
@@ -0,0 +1,2 @@
+export { default } from './BottomSheetFooter';
+export { ButtonsAlignment } from './BottomSheetFooter.types';
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories.tsx b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories.tsx
new file mode 100644
index 00000000000..785c04ee4d7
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories.tsx
@@ -0,0 +1,30 @@
+/* eslint-disable no-console */
+
+// Third party dependencies.
+import React from 'react';
+import { storiesOf } from '@storybook/react-native';
+
+// Internal dependencies.
+import BottomSheetHeader from './BottomSheetHeader';
+import { BottomSheetHeaderProps } from './BottomSheetHeader.types';
+
+export const getBottomSheetHeaderStoryProps = (): BottomSheetHeaderProps => ({
+ onBack: () => {
+ console.log('Back button clicked');
+ },
+ onClose: () => {
+ console.log('Close button clicked');
+ },
+ children: 'Super Long BottomSheetHeader Title that may span 3 lines',
+});
+
+const BottomSheetHeaderStory = () => (
+
+);
+
+storiesOf('Component Library / BottomSheets', module).add(
+ 'BottomSheetHeader',
+ BottomSheetHeaderStory,
+);
+
+export default BottomSheetHeaderStory;
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.styles.ts b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.styles.ts
new file mode 100644
index 00000000000..3c55cb94259
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.styles.ts
@@ -0,0 +1,29 @@
+// Third party dependencies.
+import { StyleSheet, ViewStyle } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../../util/theme/models';
+
+// Internal dependencies.
+import { BottomSheetHeaderStyleSheetVars } from './BottomSheetHeader.types';
+
+/**
+ * Style sheet function for BottomSheetHeader component.
+ *
+ * @param params Style sheet params.
+ * @param params.theme App theme from ThemeContext.
+ * @param params.vars Inputs that the style sheet depends on.
+ * @returns StyleSheet object.
+ */
+const styleSheet = (params: {
+ theme: Theme;
+ vars: BottomSheetHeaderStyleSheetVars;
+}) => {
+ const { vars } = params;
+ const { style } = vars;
+ return StyleSheet.create({
+ base: Object.assign({} as ViewStyle, style) as ViewStyle,
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.test.tsx b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.test.tsx
new file mode 100644
index 00000000000..e93d1a39604
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.test.tsx
@@ -0,0 +1,15 @@
+// Third party dependencies.
+import React from 'react';
+import { render } from '@testing-library/react-native';
+
+// Internal dependencies.
+import BottomSheetHeader from './BottomSheetHeader';
+
+describe('BottomSheetHeader', () => {
+ it('should render snapshot correctly', () => {
+ const wrapper = render(
+ Sample Header Title,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.tsx b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.tsx
new file mode 100644
index 00000000000..f9c7b681381
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.tsx
@@ -0,0 +1,52 @@
+/* eslint-disable react/prop-types */
+
+// Third party dependencies.
+import React from 'react';
+
+// External dependencies.
+import { useStyles } from '../../../hooks';
+import Header from '../../Header';
+import ButtonIcon, { ButtonIconVariants } from '../../Buttons/ButtonIcon';
+import { IconName } from '../../Icons/Icon';
+
+// Internal dependencies.
+import styleSheet from './BottomSheetHeader.styles';
+import { BottomSheetHeaderProps } from './BottomSheetHeader.types';
+
+const BottomSheetHeader: React.FC = ({
+ style,
+ children,
+ onBack,
+ onClose,
+ ...props
+}) => {
+ const { styles } = useStyles(styleSheet, { style });
+ const startAccessory = onBack && (
+
+ );
+
+ const endAccessory = onClose && (
+
+ );
+
+ return (
+
+ );
+};
+
+export default BottomSheetHeader;
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.types.ts b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.types.ts
new file mode 100644
index 00000000000..2bf49346cb5
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.types.ts
@@ -0,0 +1,24 @@
+// External dependencies.
+import { HeaderProps } from '../../Header/Header.types';
+
+/**
+ * BottomSheetHeader component props.
+ */
+export interface BottomSheetHeaderProps extends HeaderProps {
+ /**
+ * Optional function to trigger when pressing the back button.
+ */
+ onBack?: () => void;
+ /**
+ * Optional function to trigger when pressing the close button.
+ */
+ onClose?: () => void;
+}
+
+/**
+ * Style sheet input parameters.
+ */
+export type BottomSheetHeaderStyleSheetVars = Pick<
+ BottomSheetHeaderProps,
+ 'style'
+>;
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/README.md b/app/component-library/components/BottomSheets/BottomSheetHeader/README.md
new file mode 100644
index 00000000000..d7901edfef3
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/README.md
@@ -0,0 +1,68 @@
+# BottomSheetHeader
+
+BottomSheetHeader is a Header component specifically used for BottomSheets.
+
+## Props
+
+This component extends [BottomSheetHeaderProps](../../Header/Header.types.ts) component.
+
+### `onBack`
+
+Optional function to trigger when pressing the back button.
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| Function | No |
+
+### `onClose`
+
+Optional function to trigger when pressing the back button.
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| Function | No |
+
+## Header Props
+
+### `children`
+
+Content to wrap to display.
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| string | ReactNode | Yes |
+
+### `startAccessory`
+
+Optional prop to include content to be displayed before the title.
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| ReactNode | No |
+
+### `endAccessory`
+
+Optional prop to include content to be displayed after the title.
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| ReactNode | No |
+
+
+## Usage
+
+```javascript
+// BottomSheetHeader with String title
+ {}}
+ onClose={()=> {}}>
+ {SAMPLE_TITLE_STRING}
+;
+
+// BottomSheetHeader with custom title
+ {}}
+ onClose={()=> {}}>
+ {CUSTOM_TITLE_NODE}
+;
+```
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap b/app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap
new file mode 100644
index 00000000000..1d024babd38
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BottomSheetHeader should render snapshot correctly 1`] = `
+
+
+
+
+
+
+ Sample Header Title
+
+
+
+
+
+
+`;
diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/index.ts b/app/component-library/components/BottomSheets/BottomSheetHeader/index.ts
new file mode 100644
index 00000000000..ce95a594734
--- /dev/null
+++ b/app/component-library/components/BottomSheets/BottomSheetHeader/index.ts
@@ -0,0 +1 @@
+export { default } from './BottomSheetHeader';
diff --git a/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.styles.ts b/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.styles.ts
index d11c937a4d4..e7453f1ec1a 100644
--- a/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.styles.ts
+++ b/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.styles.ts
@@ -20,7 +20,7 @@ const styleSheet = (params: {
vars: ButtonBaseStyleSheetVars;
}) => {
const { vars, theme } = params;
- const { style, size, labelColor, width } = vars;
+ const { style, size, labelColor, width, isDisabled } = vars;
const isAutoSize: boolean = size === ButtonSize.Auto;
let widthObject;
switch (width) {
@@ -44,6 +44,7 @@ const styleSheet = (params: {
justifyContent: 'center',
borderRadius: isAutoSize ? 0 : Number(size) / 2,
paddingHorizontal: isAutoSize ? 0 : 16,
+ ...(isDisabled && { opacity: 0.5 }),
...widthObject,
} as ViewStyle,
style,
diff --git a/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.test.tsx b/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.test.tsx
index 53468e80b2a..a38a1c328fd 100644
--- a/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.test.tsx
+++ b/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.test.tsx
@@ -21,4 +21,17 @@ describe('ButtonBase', () => {
);
expect(wrapper).toMatchSnapshot();
});
+
+ it('should render correctly when disabled', () => {
+ const wrapper = shallow(
+ null}
+ />,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
});
diff --git a/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.tsx b/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.tsx
index d111df0c88d..01d04111217 100644
--- a/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.tsx
+++ b/app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.tsx
@@ -27,6 +27,7 @@ const ButtonBase = ({
style,
labelColor,
width = DEFAULT_BUTTONBASE_WIDTH,
+ isDisabled,
...props
}: ButtonBaseProps) => {
const { styles } = useStyles(styleSheet, {
@@ -34,9 +35,11 @@ const ButtonBase = ({
size,
labelColor,
width,
+ isDisabled,
});
return (
& {
size: ButtonSize;
width: ButtonWidthTypes | number;
diff --git a/app/component-library/components/Buttons/Button/foundation/ButtonBase/README.md b/app/component-library/components/Buttons/Button/foundation/ButtonBase/README.md
index 621ba3952b5..9e4119d9a99 100644
--- a/app/component-library/components/Buttons/Button/foundation/ButtonBase/README.md
+++ b/app/component-library/components/Buttons/Button/foundation/ButtonBase/README.md
@@ -70,6 +70,16 @@ Optional param to control the width of the button.
| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
| [ButtonWidthTypes](../../Button.types.ts) or number | No | ButtonWidthTypes.Auto |
+### `isDisabled`
+
+Optional boolean to disable the button.
+
+Disabled button do not trigger the onPress handler and have reduced (50%) opacity.
+
+| TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| boolean | No | false |
+
## Usage
```javascript
@@ -82,5 +92,6 @@ Optional param to control the width of the button.
onPress={SAMPLE_ONPRESS_HANDLER}
isDanger
width={ButtonWidthTypes.Auto}
+ isDisabled
/>;
```
diff --git a/app/component-library/components/Buttons/Button/foundation/ButtonBase/__snapshots__/ButtonBase.test.tsx.snap b/app/component-library/components/Buttons/Button/foundation/ButtonBase/__snapshots__/ButtonBase.test.tsx.snap
index 9790127b665..55436afb925 100644
--- a/app/component-library/components/Buttons/Button/foundation/ButtonBase/__snapshots__/ButtonBase.test.tsx.snap
+++ b/app/component-library/components/Buttons/Button/foundation/ButtonBase/__snapshots__/ButtonBase.test.tsx.snap
@@ -39,3 +39,45 @@ exports[`ButtonBase should render correctly 1`] = `
`;
+
+exports[`ButtonBase should render correctly when disabled 1`] = `
+
+
+
+ Click me!
+
+
+`;
diff --git a/app/component-library/components/Header/Header.styles.ts b/app/component-library/components/Header/Header.styles.ts
index e76f541e2ff..3290209494e 100644
--- a/app/component-library/components/Header/Header.styles.ts
+++ b/app/component-library/components/Header/Header.styles.ts
@@ -16,7 +16,7 @@ import { HeaderStyleSheetVars } from './Header.types';
* @returns StyleSheet object.
*/
const styleSheet = (params: { theme: Theme; vars: HeaderStyleSheetVars }) => {
- const { vars } = params;
+ const { vars, theme } = params;
const { style, startAccessorySize, endAccessorySize } = vars;
let accessoryWidth;
if (startAccessorySize && endAccessorySize) {
@@ -26,6 +26,7 @@ const styleSheet = (params: { theme: Theme; vars: HeaderStyleSheetVars }) => {
return StyleSheet.create({
base: Object.assign(
{
+ backgroundColor: theme.colors.background.default,
flexDirection: 'row',
alignItems: 'center',
padding: 16,
diff --git a/app/component-library/components/Header/__snapshots__/Header.test.tsx.snap b/app/component-library/components/Header/__snapshots__/Header.test.tsx.snap
index ec3b2624783..f5d5042a021 100644
--- a/app/component-library/components/Header/__snapshots__/Header.test.tsx.snap
+++ b/app/component-library/components/Header/__snapshots__/Header.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`Header should render snapshot correctly 1`] = `
style={
Object {
"alignItems": "center",
+ "backgroundColor": "#FFFFFF",
"flexDirection": "row",
"padding": 16,
}
diff --git a/app/component-library/components/Icons/Icon/Icon.assets.ts b/app/component-library/components/Icons/Icon/Icon.assets.ts
index 1c149d340b1..57a86c704f3 100644
--- a/app/component-library/components/Icons/Icon/Icon.assets.ts
+++ b/app/component-library/components/Icons/Icon/Icon.assets.ts
@@ -4,6 +4,7 @@
// DO NOT EDIT - Use generate-assets.js
///////////////////////////////////////////////////////
import { AssetByIconName, IconName } from './Icon.types';
+import Activity from './assets/activity.svg';
import AddSquare from './assets/add-square.svg';
import Add from './assets/add.svg';
import Arrow2Down from './assets/arrow-2-down.svg';
@@ -65,6 +66,7 @@ import Flag from './assets/flag.svg';
import FlashSlash from './assets/flash-slash.svg';
import Flash from './assets/flash.svg';
import Flask from './assets/flask.svg';
+import Fox from './assets/fox.svg';
import FullCircle from './assets/full-circle.svg';
import Gas from './assets/gas.svg';
import GlobalSearch from './assets/global-search.svg';
@@ -163,6 +165,7 @@ import Wifi from './assets/wifi.svg';
* Asset stored by icon name
*/
export const assetByIconName: AssetByIconName = {
+ [IconName.Activity]: Activity,
[IconName.AddSquare]: AddSquare,
[IconName.Add]: Add,
[IconName.Arrow2Down]: Arrow2Down,
@@ -224,6 +227,7 @@ export const assetByIconName: AssetByIconName = {
[IconName.FlashSlash]: FlashSlash,
[IconName.Flash]: Flash,
[IconName.Flask]: Flask,
+ [IconName.Fox]: Fox,
[IconName.FullCircle]: FullCircle,
[IconName.Gas]: Gas,
[IconName.GlobalSearch]: GlobalSearch,
diff --git a/app/component-library/components/Icons/Icon/Icon.types.ts b/app/component-library/components/Icons/Icon/Icon.types.ts
index 856ae070a74..33d7d2c291f 100644
--- a/app/component-library/components/Icons/Icon/Icon.types.ts
+++ b/app/component-library/components/Icons/Icon/Icon.types.ts
@@ -67,6 +67,7 @@ export type AssetByIconName = {
* Icon names
*/
export enum IconName {
+ Activity = 'Activity',
AddSquare = 'AddSquare',
Add = 'Add',
Arrow2Down = 'Arrow2Down',
@@ -128,6 +129,7 @@ export enum IconName {
FlashSlash = 'FlashSlash',
Flash = 'Flash',
Flask = 'Flask',
+ Fox = 'Fox',
FullCircle = 'FullCircle',
Gas = 'Gas',
GlobalSearch = 'GlobalSearch',
diff --git a/app/component-library/components/Icons/Icon/assets/activity.svg b/app/component-library/components/Icons/Icon/assets/activity.svg
new file mode 100644
index 00000000000..d66e1f64ce1
--- /dev/null
+++ b/app/component-library/components/Icons/Icon/assets/activity.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/component-library/components/Icons/Icon/assets/fox.svg b/app/component-library/components/Icons/Icon/assets/fox.svg
new file mode 100644
index 00000000000..9403fd7c133
--- /dev/null
+++ b/app/component-library/components/Icons/Icon/assets/fox.svg
@@ -0,0 +1,9 @@
+
diff --git a/app/component-library/components/Icons/Icon/assets/import.svg b/app/component-library/components/Icons/Icon/assets/import.svg
index aad98913250..fe8ad0bde52 100644
--- a/app/component-library/components/Icons/Icon/assets/import.svg
+++ b/app/component-library/components/Icons/Icon/assets/import.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/component-library/components/List/ListItem/ListItem.constants.ts b/app/component-library/components/List/ListItem/ListItem.constants.ts
new file mode 100644
index 00000000000..9b26c71cb67
--- /dev/null
+++ b/app/component-library/components/List/ListItem/ListItem.constants.ts
@@ -0,0 +1,21 @@
+/* eslint-disable import/prefer-default-export */
+
+// Internal dependencies.
+import { VerticalAlignment, ListItemProps } from './ListItem.types';
+
+// Test IDs
+export const TESTID_LISTITEM_GAP = 'listitem-gap';
+
+// Defaults
+export const DEFAULT_LISTITEM_PADDING = 16;
+export const DEFAULT_LISTITEM_BORDERRADIUS = 0;
+export const DEFAULT_LISTITEM_GAP = 16;
+export const DEFAULT_LISTITEM_VERTICALALIGNMENT = VerticalAlignment.Top;
+
+// Sample consts
+export const SAMPLE_LISTITEM_PROPS: ListItemProps = {
+ padding: DEFAULT_LISTITEM_PADDING,
+ borderRadius: DEFAULT_LISTITEM_BORDERRADIUS,
+ gap: DEFAULT_LISTITEM_GAP,
+ verticalAlignment: DEFAULT_LISTITEM_VERTICALALIGNMENT,
+};
diff --git a/app/component-library/components/List/ListItem/ListItem.stories.tsx b/app/component-library/components/List/ListItem/ListItem.stories.tsx
new file mode 100644
index 00000000000..847de1516aa
--- /dev/null
+++ b/app/component-library/components/List/ListItem/ListItem.stories.tsx
@@ -0,0 +1,79 @@
+/* eslint-disable no-console */
+
+// Third party dependencies.
+import React from 'react';
+import { storiesOf } from '@storybook/react-native';
+import { select, number } from '@storybook/addon-knobs';
+
+// External dependencies.
+import { storybookPropsGroupID } from '../../../constants/storybook.constants';
+import ListItemColumn, { WidthType } from '../ListItemColumn/';
+import Icon, { IconName } from '../../Icons/Icon';
+import Text, { TextVariant } from '../../Texts/Text';
+
+// Internal dependencies.
+import ListItem from './ListItem';
+import { ListItemProps, VerticalAlignment } from './ListItem.types';
+import {
+ DEFAULT_LISTITEM_PADDING,
+ DEFAULT_LISTITEM_BORDERRADIUS,
+ DEFAULT_LISTITEM_GAP,
+ DEFAULT_LISTITEM_VERTICALALIGNMENT,
+} from './ListItem.constants';
+
+export const getListItemStoryProps = (): ListItemProps => {
+ const paddingInput = number(
+ 'padding',
+ DEFAULT_LISTITEM_PADDING,
+ { min: 0 },
+ storybookPropsGroupID,
+ );
+
+ const borderRadiusInput = number(
+ 'borderRadius',
+ DEFAULT_LISTITEM_BORDERRADIUS,
+ { min: 0 },
+ storybookPropsGroupID,
+ );
+
+ const gapInput = number(
+ 'gap',
+ DEFAULT_LISTITEM_GAP,
+ { min: 0 },
+ storybookPropsGroupID,
+ );
+ const verticalAlignmentSelector = select(
+ 'verticalAlignment',
+ VerticalAlignment,
+ DEFAULT_LISTITEM_VERTICALALIGNMENT,
+ storybookPropsGroupID,
+ );
+
+ return {
+ padding: paddingInput,
+ borderRadius: borderRadiusInput,
+ gap: gapInput,
+ verticalAlignment: verticalAlignmentSelector,
+ };
+};
+
+const ListItemStory = () => (
+
+
+
+
+
+
+ {'Sample Title'}
+
+ {'Sample Description'}
+
+
+
+
+
+);
+
+storiesOf('Component Library / ListItem', module).add('Default', ListItemStory);
+
+export default ListItemStory;
diff --git a/app/component-library/components/List/ListItem/ListItem.styles.ts b/app/component-library/components/List/ListItem/ListItem.styles.ts
new file mode 100644
index 00000000000..63308de217d
--- /dev/null
+++ b/app/component-library/components/List/ListItem/ListItem.styles.ts
@@ -0,0 +1,47 @@
+// Third party dependencies.
+import { StyleSheet, ViewStyle } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../../util/theme/models';
+
+// Internal dependencies.
+import { VerticalAlignment, ListItemStyleSheetVars } from './ListItem.types';
+
+/**
+ * Style sheet function for ListItem component.
+ *
+ * @param params Style sheet params.
+ * @param params.theme App theme from ThemeContext.
+ * @param params.vars Inputs that the style sheet depends on.
+ * @returns StyleSheet object.
+ */
+const styleSheet = (params: { theme: Theme; vars: ListItemStyleSheetVars }) => {
+ const { vars } = params;
+ const { style, padding, borderRadius, verticalAlignment } = vars;
+ let alignItems;
+ switch (verticalAlignment) {
+ case VerticalAlignment.Center:
+ alignItems = 'center';
+ break;
+ case VerticalAlignment.Bottom:
+ alignItems = 'flex-end';
+ break;
+ case VerticalAlignment.Top:
+ default:
+ alignItems = 'flex-start';
+ }
+
+ return StyleSheet.create({
+ base: Object.assign(
+ {
+ flexDirection: 'row',
+ alignItems,
+ padding,
+ borderRadius,
+ } as ViewStyle,
+ style,
+ ) as ViewStyle,
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components/List/ListItem/ListItem.test.tsx b/app/component-library/components/List/ListItem/ListItem.test.tsx
new file mode 100644
index 00000000000..9583305ce0e
--- /dev/null
+++ b/app/component-library/components/List/ListItem/ListItem.test.tsx
@@ -0,0 +1,119 @@
+// Third party dependencies.
+import React from 'react';
+import { render } from '@testing-library/react-native';
+import { View } from 'react-native';
+
+// External dependencies.
+
+// Internal dependencies.
+import ListItem from './ListItem';
+import {
+ DEFAULT_LISTITEM_PADDING,
+ DEFAULT_LISTITEM_BORDERRADIUS,
+ DEFAULT_LISTITEM_GAP,
+ TESTID_LISTITEM_GAP,
+} from './ListItem.constants';
+import { VerticalAlignment } from './ListItem.types';
+
+describe('ListItem', () => {
+ it('should render snapshot correctly', () => {
+ const wrapper = render(
+
+
+ ,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should render the correct default padding', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('none').props.style.padding).toBe(
+ DEFAULT_LISTITEM_PADDING,
+ );
+ });
+
+ it('should render the given padding', () => {
+ const givenPadding = 12;
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('none').props.style.padding).toBe(givenPadding);
+ });
+
+ it('should render the correct default borderRadius', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('none').props.style.borderRadius).toBe(
+ DEFAULT_LISTITEM_BORDERRADIUS,
+ );
+ });
+
+ it('should render the given borderRadius', () => {
+ const givenBorderRadius = 12;
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('none').props.style.borderRadius).toBe(givenBorderRadius);
+ });
+
+ it('should render the correct default gap', () => {
+ const { getByTestId } = render(
+
+
+
+ ,
+ );
+ expect(getByTestId(TESTID_LISTITEM_GAP).props.style.width).toBe(
+ DEFAULT_LISTITEM_GAP,
+ );
+ });
+
+ it('should render the given gap', () => {
+ const givenGap = 20;
+ const { getByTestId } = render(
+
+
+
+ ,
+ );
+ expect(getByTestId(TESTID_LISTITEM_GAP).props.style.width).toBe(givenGap);
+ });
+
+ it('should not render a gap with only 1 child', () => {
+ const { queryByTestId } = render(
+
+
+ ,
+ );
+ expect(queryByTestId(TESTID_LISTITEM_GAP)).toBeNull();
+ });
+
+ it('should render the correct default verticalAlignment', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('none').props.style.alignItems).toBe('flex-start');
+ });
+
+ it('should render the given verticalAlignment', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
+ expect(getByRole('none').props.style.alignItems).toBe('center');
+ });
+});
diff --git a/app/component-library/components/List/ListItem/ListItem.tsx b/app/component-library/components/List/ListItem/ListItem.tsx
new file mode 100644
index 00000000000..13b33a0f035
--- /dev/null
+++ b/app/component-library/components/List/ListItem/ListItem.tsx
@@ -0,0 +1,53 @@
+/* eslint-disable react/prop-types */
+
+// Third party dependencies.
+import React from 'react';
+import { View } from 'react-native';
+
+// External dependencies.
+import { useStyles } from '../../../hooks';
+
+// Internal dependencies.
+import styleSheet from './ListItem.styles';
+import { ListItemProps } from './ListItem.types';
+import {
+ DEFAULT_LISTITEM_PADDING,
+ DEFAULT_LISTITEM_BORDERRADIUS,
+ DEFAULT_LISTITEM_GAP,
+ DEFAULT_LISTITEM_VERTICALALIGNMENT,
+ TESTID_LISTITEM_GAP,
+} from './ListItem.constants';
+
+const ListItem: React.FC = ({
+ style,
+ children,
+ padding = DEFAULT_LISTITEM_PADDING,
+ borderRadius = DEFAULT_LISTITEM_BORDERRADIUS,
+ gap = DEFAULT_LISTITEM_GAP,
+ verticalAlignment = DEFAULT_LISTITEM_VERTICALALIGNMENT,
+}) => {
+ const { styles } = useStyles(styleSheet, {
+ style,
+ padding,
+ borderRadius,
+ verticalAlignment,
+ });
+ return (
+
+ {React.Children.map(children, (child, index) => (
+ <>
+ {index > 0 && (
+
+ )}
+ {child}
+ >
+ ))}
+
+ );
+};
+
+export default ListItem;
diff --git a/app/component-library/components/List/ListItem/ListItem.types.ts b/app/component-library/components/List/ListItem/ListItem.types.ts
new file mode 100644
index 00000000000..57771679505
--- /dev/null
+++ b/app/component-library/components/List/ListItem/ListItem.types.ts
@@ -0,0 +1,45 @@
+// Third party dependencies.
+import { ViewProps } from 'react-native';
+
+/**
+ * Vertical Alignment Options.
+ */
+export enum VerticalAlignment {
+ Top = 'Top',
+ Center = 'Center',
+ Bottom = 'Bottom',
+}
+
+/**
+ * ListItem component props.
+ */
+export interface ListItemProps extends ViewProps {
+ /**
+ * Content to wrap to display.
+ */
+ children?: React.ReactNode;
+ /**
+ * Optional prop to configure the padding of the ListItem.
+ */
+ padding?: number | string | undefined;
+ /**
+ * Optional prop to configure the borderRadius of the ListItem.
+ */
+ borderRadius?: number | string | undefined;
+ /**
+ * Optional prop to configure the gap between items inside the ListItem.
+ */
+ gap?: number | string | undefined;
+ /**
+ * Optional prop to configure the vertical alignment between items inside the ListItem.
+ */
+ verticalAlignment?: VerticalAlignment;
+}
+
+/**
+ * Style sheet input parameters.
+ */
+export type ListItemStyleSheetVars = Pick<
+ ListItemProps,
+ 'style' | 'padding' | 'borderRadius' | 'verticalAlignment'
+>;
diff --git a/app/component-library/components/List/ListItem/README.md b/app/component-library/components/List/ListItem/README.md
new file mode 100644
index 00000000000..436312256df
--- /dev/null
+++ b/app/component-library/components/List/ListItem/README.md
@@ -0,0 +1,58 @@
+# ListItem
+
+ListItem is a wrapper component used to display an individual item within a list.
+
+## Props
+
+This component extends [ViewProps](https://reactnative.dev/docs/view-style-props) from React Native's [View](https://reactnative.dev/docs/view) component.
+
+### `children`
+
+Content to wrap to display.
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| ReactNode | No |
+
+### `padding`
+
+Optional prop to configure the padding of the ListItem.
+
+| TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| number or string | No | 16 |
+
+### `borderRadius`
+
+Optional prop to configure the borderRadius of the ListItem.
+
+| TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| number or string | No | 0 |
+
+### `gap`
+
+Optional prop to configure the gap between items inside the ListItem.
+
+| TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| number or string | No | 16 |
+
+### `verticalAlignment`
+
+Optional prop to configure the vertical alignment between items inside the ListItem.
+
+| TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| VerticalAlignment | No | VerticalAlignment.Top |
+
+## Usage
+
+```javascript
+
+ {SAMPLE_CHILDREN_COMPONENT}
+
+```
diff --git a/app/component-library/components/List/ListItem/__snapshots__/ListItem.test.tsx.snap b/app/component-library/components/List/ListItem/__snapshots__/ListItem.test.tsx.snap
new file mode 100644
index 00000000000..7e73c8f3dac
--- /dev/null
+++ b/app/component-library/components/List/ListItem/__snapshots__/ListItem.test.tsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ListItem should render snapshot correctly 1`] = `
+
+
+
+`;
diff --git a/app/component-library/components/List/ListItem/index.ts b/app/component-library/components/List/ListItem/index.ts
new file mode 100644
index 00000000000..61c12ad8b83
--- /dev/null
+++ b/app/component-library/components/List/ListItem/index.ts
@@ -0,0 +1,2 @@
+export { default } from './ListItem';
+export { VerticalAlignment } from './ListItem.types';
diff --git a/app/component-library/components/List/ListItemColumn/ListItemColumn.constants.ts b/app/component-library/components/List/ListItemColumn/ListItemColumn.constants.ts
new file mode 100644
index 00000000000..4bb8ea1daf9
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/ListItemColumn.constants.ts
@@ -0,0 +1,15 @@
+/* eslint-disable import/prefer-default-export */
+
+// Internal dependencies.
+import { ListItemColumnProps, WidthType } from './ListItemColumn.types';
+
+// Test IDs
+export const TESTID_LISTITEMCOLUMN = 'listitemcolumn';
+
+// Defaults
+export const DEFAULT_LISTITEMCOLUMN_WIDTHTYPE = WidthType.Auto;
+
+// Sample consts
+export const SAMPLE_LISTITEMCOLUMN_PROPS: ListItemColumnProps = {
+ widthType: DEFAULT_LISTITEMCOLUMN_WIDTHTYPE,
+};
diff --git a/app/component-library/components/List/ListItemColumn/ListItemColumn.styles.ts b/app/component-library/components/List/ListItemColumn/ListItemColumn.styles.ts
new file mode 100644
index 00000000000..793e0b03e09
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/ListItemColumn.styles.ts
@@ -0,0 +1,38 @@
+// Third party dependencies.
+import { StyleSheet, ViewStyle } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../../util/theme/models';
+
+// Internal dependencies.
+import {
+ ListItemColumnStyleSheetVars,
+ WidthType,
+} from './ListItemColumn.types';
+
+/**
+ * Style sheet function for ListItemColumn component.
+ *
+ * @param params Style sheet params.
+ * @param params.theme App theme from ThemeContext.
+ * @param params.vars Inputs that the style sheet depends on.
+ * @returns StyleSheet object.
+ */
+const styleSheet = (params: {
+ theme: Theme;
+ vars: ListItemColumnStyleSheetVars;
+}) => {
+ const { vars } = params;
+ const { style, widthType } = vars;
+
+ return StyleSheet.create({
+ base: Object.assign(
+ {
+ flex: widthType === WidthType.Auto ? -1 : 1,
+ } as ViewStyle,
+ style,
+ ) as ViewStyle,
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components/List/ListItemColumn/ListItemColumn.test.tsx b/app/component-library/components/List/ListItemColumn/ListItemColumn.test.tsx
new file mode 100644
index 00000000000..ef961b2dbb6
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/ListItemColumn.test.tsx
@@ -0,0 +1,60 @@
+// Third party dependencies.
+import React from 'react';
+import { shallow } from 'enzyme';
+import { View } from 'react-native';
+
+// External dependencies.
+
+// Internal dependencies.
+import ListItemColumn from './ListItemColumn';
+import {
+ DEFAULT_LISTITEMCOLUMN_WIDTHTYPE,
+ TESTID_LISTITEMCOLUMN,
+} from './ListItemColumn.constants';
+import { WidthType } from './ListItemColumn.types';
+
+describe('ListItemColumn', () => {
+ it('should render snapshot correctly', () => {
+ const wrapper = shallow(
+
+
+ ,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+ it('should render component correctly', () => {
+ const wrapper = shallow(
+
+
+ ,
+ );
+ const listItemColumnComponent = wrapper.findWhere(
+ (node) => node.prop('testID') === TESTID_LISTITEMCOLUMN,
+ );
+ expect(listItemColumnComponent.exists()).toBe(true);
+ });
+
+ it('should render the correct default widthType', () => {
+ const wrapper = shallow(
+
+
+ ,
+ );
+ const listItemColumnComponent = wrapper.findWhere(
+ (node) => node.prop('testID') === TESTID_LISTITEMCOLUMN,
+ );
+ expect(listItemColumnComponent.props().style.flex).toBe(-1);
+ });
+
+ it('should render the given widthType', () => {
+ const wrapper = shallow(
+
+
+ ,
+ );
+ const listItemColumnComponent = wrapper.findWhere(
+ (node) => node.prop('testID') === TESTID_LISTITEMCOLUMN,
+ );
+ expect(listItemColumnComponent.props().style.flex).toBe(1);
+ });
+});
diff --git a/app/component-library/components/List/ListItemColumn/ListItemColumn.tsx b/app/component-library/components/List/ListItemColumn/ListItemColumn.tsx
new file mode 100644
index 00000000000..8e535cb5fd7
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/ListItemColumn.tsx
@@ -0,0 +1,35 @@
+/* eslint-disable react/prop-types */
+
+// Third party dependencies.
+import React from 'react';
+import { View } from 'react-native';
+
+// External dependencies.
+import { useStyles } from '../../../hooks';
+
+// Internal dependencies.
+import styleSheet from './ListItemColumn.styles';
+import { ListItemColumnProps } from './ListItemColumn.types';
+import {
+ DEFAULT_LISTITEMCOLUMN_WIDTHTYPE,
+ TESTID_LISTITEMCOLUMN,
+} from './ListItemColumn.constants';
+
+const ListItemColumn: React.FC = ({
+ style,
+ children,
+ widthType = DEFAULT_LISTITEMCOLUMN_WIDTHTYPE,
+}) => {
+ const { styles } = useStyles(styleSheet, {
+ style,
+ widthType,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ListItemColumn;
diff --git a/app/component-library/components/List/ListItemColumn/ListItemColumn.types.ts b/app/component-library/components/List/ListItemColumn/ListItemColumn.types.ts
new file mode 100644
index 00000000000..19fcc49c9f9
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/ListItemColumn.types.ts
@@ -0,0 +1,32 @@
+// Third party dependencies.
+import { ViewProps } from 'react-native';
+
+/**
+ * Width variants.
+ */
+export enum WidthType {
+ Auto = 'auto',
+ Fill = 'fill',
+}
+
+/**
+ * ListItemColumn component props.
+ */
+export interface ListItemColumnProps extends ViewProps {
+ /**
+ * Optional prop for content to wrap to display.
+ */
+ children?: React.ReactNode;
+ /**
+ * Optional prop to configure the width of the column.
+ */
+ widthType?: WidthType;
+}
+
+/**
+ * Style sheet input parameters.
+ */
+export type ListItemColumnStyleSheetVars = Pick<
+ ListItemColumnProps,
+ 'style' | 'widthType'
+>;
diff --git a/app/component-library/components/List/ListItemColumn/README.md b/app/component-library/components/List/ListItemColumn/README.md
new file mode 100644
index 00000000000..35019b85da0
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/README.md
@@ -0,0 +1,31 @@
+# ListItemColumn
+
+ListItemColumn is a wrapper component to be placed in a ListItem.
+
+## Props
+
+This component extends [ViewProps](https://reactnative.dev/docs/view-style-props) from React Native's [View](https://reactnative.dev/docs/view) component.
+
+### `children`
+
+Optional prop for content to wrap to display.
+
+| TYPE | REQUIRED |
+| :-------------------------------------------------- | :------------------------------------------------------ |
+| ReactNode | No |
+
+### `widthType`
+
+Optional prop to configure the width of the column.
+
+| TYPE | REQUIRED | DEFAULT |
+| :-------------------------------------------------- | :------------------------------------------------------ | :----------------------------------------------------- |
+| WidthType | No | WidthType.Auto |
+
+## Usage
+
+```javascript
+
+ {SAMPLE_CHILDREN_COMPONENT}
+
+```
diff --git a/app/component-library/components/List/ListItemColumn/__snapshots__/ListItemColumn.test.tsx.snap b/app/component-library/components/List/ListItemColumn/__snapshots__/ListItemColumn.test.tsx.snap
new file mode 100644
index 00000000000..5fe51e93c93
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/__snapshots__/ListItemColumn.test.tsx.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ListItemColumn should render snapshot correctly 1`] = `
+
+
+
+`;
diff --git a/app/component-library/components/List/ListItemColumn/index.ts b/app/component-library/components/List/ListItemColumn/index.ts
new file mode 100644
index 00000000000..fe2f11edc12
--- /dev/null
+++ b/app/component-library/components/List/ListItemColumn/index.ts
@@ -0,0 +1,2 @@
+export { default } from './ListItemColumn';
+export { WidthType } from './ListItemColumn.types';
diff --git a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts
index bd7379cb5be..c3e0fc1ae3c 100644
--- a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts
+++ b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts
@@ -10,4 +10,6 @@ export const ICON_BY_TAB_BAR_ICON_KEY: IconByTabBarIconKey = {
[TabBarIconKey.Wallet]: IconName.Wallet,
[TabBarIconKey.Browser]: IconName.Explore,
[TabBarIconKey.Actions]: IconName.SwapVertival,
+ [TabBarIconKey.Activity]: IconName.Activity,
+ [TabBarIconKey.Setting]: IconName.Setting,
};
diff --git a/app/component-library/components/Navigation/TabBar/TabBar.test.tsx b/app/component-library/components/Navigation/TabBar/TabBar.test.tsx
index 734f0380292..69749e9467d 100644
--- a/app/component-library/components/Navigation/TabBar/TabBar.test.tsx
+++ b/app/component-library/components/Navigation/TabBar/TabBar.test.tsx
@@ -53,6 +53,8 @@ describe('TabBar', () => {
{ key: '1', name: 'Tab 1' },
{ key: '2', name: 'Tab 2' },
{ key: '3', name: 'Tab 3' },
+ { key: '4', name: 'Tab 4' },
+ { key: '5', name: 'Tab 5' },
],
};
const descriptors = {
@@ -63,17 +65,29 @@ describe('TabBar', () => {
},
},
'2': {
+ options: {
+ tabBarIconKey: TabBarIconKey.Activity,
+ rootScreenName: Routes.TRANSACTIONS_VIEW,
+ },
+ },
+ '3': {
options: {
tabBarIconKey: TabBarIconKey.Actions,
rootScreenName: Routes.MODAL.WALLET_ACTIONS,
},
},
- '3': {
+ '4': {
options: {
tabBarIconKey: TabBarIconKey.Browser,
rootScreenName: Routes.BROWSER_VIEW,
},
},
+ '5': {
+ options: {
+ tabBarIconKey: TabBarIconKey.Setting,
+ rootScreenName: Routes.SETTINGS_VIEW,
+ },
+ },
};
it('renders correctly', () => {
@@ -118,5 +132,13 @@ describe('TabBar', () => {
screen: Routes.MODAL.WALLET_ACTIONS,
},
);
+
+ fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Activity}`));
+ expect(navigation.navigate).toHaveBeenCalledWith(Routes.TRANSACTIONS_VIEW);
+
+ fireEvent.press(getByTestId(`tab-bar-item-${TabBarIconKey.Setting}`));
+ expect(navigation.navigate).toHaveBeenCalledWith(Routes.SETTINGS_VIEW, {
+ screen: 'Settings',
+ });
});
});
diff --git a/app/component-library/components/Navigation/TabBar/TabBar.tsx b/app/component-library/components/Navigation/TabBar/TabBar.tsx
index 58920794921..5d66af09737 100644
--- a/app/component-library/components/Navigation/TabBar/TabBar.tsx
+++ b/app/component-library/components/Navigation/TabBar/TabBar.tsx
@@ -84,6 +84,14 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => {
navigation.navigate(Routes.BROWSER.HOME, {
screen: Routes.BROWSER_VIEW,
});
+ break;
+ case Routes.TRANSACTIONS_VIEW:
+ navigation.navigate(Routes.TRANSACTIONS_VIEW);
+ break;
+ case Routes.SETTINGS_VIEW:
+ navigation.navigate(Routes.SETTINGS_VIEW, {
+ screen: 'Settings',
+ });
}
};
diff --git a/app/component-library/components/Navigation/TabBar/TabBar.types.ts b/app/component-library/components/Navigation/TabBar/TabBar.types.ts
index dd8a07b4186..59bef4e34f8 100644
--- a/app/component-library/components/Navigation/TabBar/TabBar.types.ts
+++ b/app/component-library/components/Navigation/TabBar/TabBar.types.ts
@@ -18,6 +18,8 @@ export enum TabBarIconKey {
Wallet = 'Wallet',
Browser = 'Browser',
Actions = 'Actions',
+ Activity = 'Activity',
+ Setting = 'Setting',
}
/**
diff --git a/app/component-library/components/Navigation/TabBar/__snapshots__/TabBar.test.tsx.snap b/app/component-library/components/Navigation/TabBar/__snapshots__/TabBar.test.tsx.snap
index bdff99b2937..d5dde280fdd 100644
--- a/app/component-library/components/Navigation/TabBar/__snapshots__/TabBar.test.tsx.snap
+++ b/app/component-library/components/Navigation/TabBar/__snapshots__/TabBar.test.tsx.snap
@@ -66,6 +66,43 @@ Array [
/>
+
+
+
+
+
+
+
+
+
+
,
]
`;
diff --git a/app/component-library/components/Select/Multiselect/MultiselectItem/MultiselectItem.tsx b/app/component-library/components/Select/Multiselect/MultiselectItem/MultiselectItem.tsx
index 8fd19147e7f..537b9a798d9 100644
--- a/app/component-library/components/Select/Multiselect/MultiselectItem/MultiselectItem.tsx
+++ b/app/component-library/components/Select/Multiselect/MultiselectItem/MultiselectItem.tsx
@@ -32,7 +32,11 @@ const MultiselectItem: React.FC = ({
return (
{renderUnderlay()}
-
+
{children}
);
diff --git a/app/component-library/components/Sheet/SheetBottom/SheetBottom.test.tsx b/app/component-library/components/Sheet/SheetBottom/SheetBottom.test.tsx
new file mode 100644
index 00000000000..ecd5e42ca89
--- /dev/null
+++ b/app/component-library/components/Sheet/SheetBottom/SheetBottom.test.tsx
@@ -0,0 +1,48 @@
+// Third party dependencies.
+import React from 'react';
+import { shallow } from 'enzyme';
+import { Platform, View } from 'react-native';
+
+// Internal dependencies.
+import SheetBottom from './SheetBottom';
+
+jest.mock('react-native-safe-area-context', () => {
+ // using disting digits for mock rects to make sure they are not mixed up
+ const inset = { top: 1, right: 2, bottom: 3, left: 4 };
+ const frame = { width: 5, height: 6, x: 7, y: 8 };
+ return {
+ SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children),
+ SafeAreaConsumer: jest
+ .fn()
+ .mockImplementation(({ children }) => children(inset)),
+ useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
+ useSafeAreaFrame: jest.fn().mockImplementation(() => frame),
+ };
+});
+
+jest.mock('@react-navigation/native', () => {
+ const actualNav = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualNav,
+ useNavigation: () => ({
+ navigate: jest.fn(),
+ }),
+ };
+});
+
+describe('SheetBottom', () => {
+ enum PlatformEnum {
+ iOS = 'ios',
+ Android = 'android',
+ }
+ const platforms = [PlatformEnum.iOS, PlatformEnum.Android];
+ test.each(platforms)('should render correctly on %s', (platform) => {
+ Platform.OS = platform;
+ const wrapper = shallow(
+
+
+ ,
+ );
+ expect(wrapper).toMatchSnapshot(platform);
+ });
+});
diff --git a/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx b/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx
index 7f57e761eed..6d5d9d70b78 100644
--- a/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx
+++ b/app/component-library/components/Sheet/SheetBottom/SheetBottom.tsx
@@ -12,7 +12,9 @@ import React, {
} from 'react';
import {
BackHandler,
+ KeyboardAvoidingView,
LayoutChangeEvent,
+ Platform,
TouchableOpacity,
useWindowDimensions,
View,
@@ -30,7 +32,10 @@ import Animated, {
useSharedValue,
withTiming,
} from 'react-native-reanimated';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import {
+ useSafeAreaFrame,
+ useSafeAreaInsets,
+} from 'react-native-safe-area-context';
import { debounce } from 'lodash';
// External dependencies.
@@ -67,6 +72,7 @@ const SheetBottom = forwardRef(
const { top: screenTopPadding, bottom: screenBottomPadding } =
useSafeAreaInsets();
const { height: screenHeight } = useWindowDimensions();
+ const { y: frameY } = useSafeAreaFrame();
const { styles } = useStyles(styleSheet, {
maxSheetHeight:
screenHeight - screenTopPadding - reservedMinOverlayHeight,
@@ -233,7 +239,14 @@ const SheetBottom = forwardRef(
const renderNotch = () => isInteractable && ;
return (
-
+
(
{children}
-
+
);
},
);
diff --git a/app/component-library/components/Sheet/SheetBottom/__snapshots__/SheetBottom.test.tsx.snap b/app/component-library/components/Sheet/SheetBottom/__snapshots__/SheetBottom.test.tsx.snap
new file mode 100644
index 00000000000..589993a4387
--- /dev/null
+++ b/app/component-library/components/Sheet/SheetBottom/__snapshots__/SheetBottom.test.tsx.snap
@@ -0,0 +1,173 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SheetBottom should render correctly on android: android 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`SheetBottom should render correctly on ios: ios 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/Base/hooks/useRemoteResourceExists.tsx b/app/components/Base/hooks/useRemoteResourceExists.tsx
deleted file mode 100644
index a2407a3c6b0..00000000000
--- a/app/components/Base/hooks/useRemoteResourceExists.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useState, useEffect } from 'react';
-
-/**
- * @typedef {Boolean} resourceExists boolean value that represent wether the remote resource exists or not
- * @typedef {Boolean} isLoading boolean value that indicates when the promise is completed
- */
-
-/**
- * Hook to handle the remote state of a resource
- * @param {String} uri Resource URI
- * @return {Boolean[]} `[resourceExists, isLoading]`
- */
-const useRemoteResourceExists = (uri: string): boolean[] => {
- const [resourceExists, setResourceExists] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
-
- const fetchStatus = async (innerUri: string): Promise => {
- fetch(innerUri, { method: 'HEAD' })
- .then((res) => setResourceExists(res.status === 200))
- .catch(() => setResourceExists(false))
- .finally(() => setIsLoading(false));
- };
-
- useEffect(() => {
- fetchStatus(uri);
- }, [uri]);
-
- return [resourceExists, isLoading];
-};
-
-export default useRemoteResourceExists;
diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index 7eeddea060d..e7dc3164eda 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -79,6 +79,7 @@ import WalletResetNeeded from '../../Views/RestoreWallet/WalletResetNeeded';
import SDKLoadingModal from '../../Views/SDKLoadingModal/SDKLoadingModal';
import SDKFeedbackModal from '../../Views/SDKFeedbackModal/SDKFeedbackModal';
import AccountActions from '../../../components/Views/AccountActions';
+import EthSignFriction from '../../../components/Views/Settings/AdvancedSettings/EthSignFriction';
import WalletActions from '../../Views/WalletActions';
import NetworkSelector from '../../../components/Views/NetworkSelector';
import EditAccountName from '../../Views/EditAccountName/EditAccountName';
@@ -154,7 +155,6 @@ const OnboardingNav = () => (
component={OptinMetrics}
options={OptinMetrics.navigationOptions}
/>
-
);
@@ -519,6 +519,10 @@ const App = ({ userLoggedIn }) => {
name={Routes.SHEET.ACCOUNT_ACTIONS}
component={AccountActions}
/>
+
);
@@ -559,6 +563,18 @@ const App = ({ userLoggedIn }) => {
);
+ // eslint-disable-next-line react/prop-types
+ const AddNetworkFlow = ({ route }) => (
+
+
+
+ );
+
return (
// do not render unless a route is defined
(route && (
@@ -624,6 +640,12 @@ const App = ({ userLoggedIn }) => {
+
diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index dd4922ec4c4..73d412210e0 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -13,7 +13,6 @@ import AdvancedSettings from '../../Views/Settings/AdvancedSettings';
import SecuritySettings from '../../Views/Settings/SecuritySettings';
import ExperimentalSettings from '../../Views/Settings/ExperimentalSettings';
import NetworksSettings from '../../Views/Settings/NetworksSettings';
-import NetworkSettings from '../../Views/Settings/NetworksSettings/NetworkSettings';
import AppInformation from '../../Views/Settings/AppInformation';
import Contacts from '../../Views/Settings/Contacts';
import Wallet from '../../Views/Wallet';
@@ -56,7 +55,6 @@ import CheckoutWebView from '../../UI/FiatOnRampAggregator/Views/Checkout';
import OnRampSettings from '../../UI/FiatOnRampAggregator/Views/Settings';
import OnrampAddActivationKey from '../../UI/FiatOnRampAggregator/Views/Settings/AddActivationKey';
import Regions from '../../UI/FiatOnRampAggregator/Views/Regions';
-import ThemeSettings from '../../Views/ThemeSettings';
import { colors as importedColors } from '../../../styles/common';
import OrderDetails from '../../UI/FiatOnRampAggregator/Views/OrderDetails';
import TabBar from '../../../component-library/components/Navigation/TabBar';
@@ -200,6 +198,104 @@ const BrowserFlow = () => (
export const DrawerContext = React.createContext({ drawerRef: null });
+const SettingsFlow = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
const HomeTabs = () => {
const drawerRef = useRef(null);
const [isKeyboardHidden, setIsKeyboardHidden] = useState(true);
@@ -269,6 +365,22 @@ const HomeTabs = () => {
},
rootScreenName: Routes.BROWSER_VIEW,
},
+ activity: {
+ tabBarIconKey: TabBarIconKey.Activity,
+ callback: () => {
+ AnalyticsV2.trackEvent(
+ MetaMetricsEvents.NAVIGATION_TAPS_TRANSACTION_HISTORY,
+ );
+ },
+ rootScreenName: Routes.TRANSACTIONS_VIEW,
+ },
+ settings: {
+ tabBarIconKey: TabBarIconKey.Setting,
+ callback: () => {
+ AnalyticsV2.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_SETTINGS);
+ },
+ rootScreenName: Routes.SETTINGS_VIEW,
+ },
};
useEffect(() => {
@@ -309,6 +421,11 @@ const HomeTabs = () => {
options={options.home}
component={WalletTabModalFlow}
/>
+
{
options={options.browser}
component={BrowserFlow}
/>
+
+
@@ -336,123 +459,6 @@ const Webview = () => (
);
-const SettingsFlow = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const SettingsModalStack = () => (
-
-
-
-
-);
-
const SendView = () => (
(
-
-
{
const { colors } = useTheme();
const [showPendingApproval, setShowPendingApproval] = useState(false);
+ const [transactionModalType, setTransactionModalType] = useState(undefined);
const [walletConnectRequestInfo, setWalletConnectRequestInfo] =
useState(undefined);
const [currentPageMeta, setCurrentPageMeta] = useState({});
@@ -84,16 +81,18 @@ const RootRPCMethodsUI = (props) => {
const [hostToApprove, setHostToApprove] = useState(null);
- const [watchAsset, setWatchAsset] = useState(false);
- const [suggestedAssetMeta, setSuggestedAssetMeta] = useState(undefined);
+ const [watchAsset, setWatchAsset] = useState(undefined);
const [signMessageParams, setSignMessageParams] = useState(undefined);
const setTransactionObject = props.setTransactionObject;
- const toggleApproveModal = props.toggleApproveModal;
- const toggleDappTransactionModal = props.toggleDappTransactionModal;
const setEtherTransaction = props.setEtherTransaction;
+ const TransactionModalType = {
+ Transaction: 'transaction',
+ Dapp: 'dapp',
+ };
+
// Reject pending approval using MetaMask SDK.
const rejectPendingApproval = (id, error) => {
const { ApprovalController } = Engine.context;
@@ -361,36 +360,27 @@ const RootRPCMethodsUI = (props) => {
data.substr(0, 10) === APPROVE_FUNCTION_SIGNATURE &&
(!value || isZeroValue(value))
) {
- toggleApproveModal();
+ setTransactionModalType(TransactionModalType.Transaction);
} else {
- toggleDappTransactionModal();
+ setTransactionModalType(TransactionModalType.Dapp);
}
}
},
[
- props.tokens,
props.chainId,
- setEtherTransaction,
- setTransactionObject,
- toggleApproveModal,
- toggleDappTransactionModal,
+ props.tokens,
autoSign,
+ setTransactionObject,
tokenList,
+ setEtherTransaction,
+ TransactionModalType.Transaction,
+ TransactionModalType.Dapp,
],
);
const renderQRSigningModal = () => {
- const {
- isSigningQRObject,
- QRState,
- approveModalVisible,
- dappTransactionModalVisible,
- } = props;
- const shouldRenderThisModal =
- !showPendingApproval &&
- !approveModalVisible &&
- !dappTransactionModalVisible &&
- isSigningQRObject;
+ const { isSigningQRObject, QRState } = props;
+ const shouldRenderThisModal = !showPendingApproval && isSigningQRObject;
return (
shouldRenderThisModal && (
@@ -447,19 +437,39 @@ const RootRPCMethodsUI = (props) => {
);
};
- const renderDappTransactionModal = () =>
- props.dappTransactionModalVisible && (
-
+ const hideTransactionModal = () => {
+ setShowPendingApproval(false);
+ };
+
+ const showTransactionApproval = () =>
+ showPendingApproval?.type === ApprovalTypes.TRANSACTION;
+
+ const renderDappTransactionModal = () => {
+ const transactionApprovalVisible = showTransactionApproval();
+ return (
+ transactionApprovalVisible &&
+ transactionModalType === TransactionModalType.Dapp && (
+
+ )
);
+ };
- const renderApproveModal = () =>
- props.approveModalVisible && (
-
+ const renderApproveModal = () => {
+ const transactionApprovalVisible = showTransactionApproval();
+ return (
+ transactionApprovalVisible &&
+ transactionModalType === TransactionModalType.Transaction && (
+
+ )
);
+ };
const onAddCustomNetworkReject = () => {
setShowPendingApproval(false);
@@ -590,38 +600,58 @@ const RootRPCMethodsUI = (props) => {
);
/**
- * On rejection addinga an asset
+ * On confirming watching an asset
+ */
+ const onWatchAssetConfirm = () => {
+ acceptPendingApproval(watchAsset.id, watchAsset.data);
+ setShowPendingApproval(false);
+ setWatchAsset(undefined);
+ };
+
+ /**
+ * On rejecting watching an asset
*/
- const onCancelWatchAsset = () => {
- setWatchAsset(false);
+ const onWatchAssetReject = () => {
+ rejectPendingApproval(
+ watchAsset.id,
+ ethErrors.provider.userRejectedRequest(),
+ );
+ setShowPendingApproval(false);
+ setWatchAsset(undefined);
};
/**
* Render the add asset modal
*/
- const renderWatchAssetModal = () => (
-
-
-
- );
+ const renderWatchAssetModal = () => {
+ if (!watchAsset) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
const onSign = () => {
setSignMessageParams(undefined);
@@ -719,6 +749,19 @@ const RootRPCMethodsUI = (props) => {
origin: request.origin,
});
break;
+ case ApprovalTypes.WATCH_ASSET:
+ setWatchAsset({ data: requestData, id: request.id });
+ showPendingApprovalModal({
+ type: ApprovalTypes.WATCH_ASSET,
+ origin: request.origin,
+ });
+ break;
+ case ApprovalTypes.TRANSACTION:
+ showPendingApprovalModal({
+ type: ApprovalTypes.TRANSACTION,
+ origin: request.origin,
+ });
+ break;
default:
break;
}
@@ -735,14 +778,6 @@ const RootRPCMethodsUI = (props) => {
handlePendingApprovals,
);
- Engine.context.TokensController.hub.on(
- 'pendingSuggestedAsset',
- (suggestedAssetMeta) => {
- setSuggestedAssetMeta(suggestedAssetMeta);
- setWatchAsset(true);
- },
- );
-
return function cleanup() {
Engine.context.TokensController.hub.removeAllListeners();
Engine.controllerMessenger.unsubscribe(
@@ -787,22 +822,6 @@ RootRPCMethodsUI.propTypes = {
* Array of ERC20 assets
*/
tokens: PropTypes.array,
- /**
- /* Hides or shows dApp transaction modal
- */
- toggleDappTransactionModal: PropTypes.func,
- /**
- /* Hides or shows approve modal
- */
- toggleApproveModal: PropTypes.func,
- /**
- /* dApp transaction modal visible or not
- */
- dappTransactionModalVisible: PropTypes.bool,
- /**
- /* Token approve modal visible or not
- */
- approveModalVisible: PropTypes.bool,
/**
* Selected address
*/
@@ -825,8 +844,6 @@ const mapStateToProps = (state) => ({
state.engine.backgroundState.PreferencesController.selectedAddress,
chainId: selectChainId(state),
tokens: state.engine.backgroundState.TokensController.tokens,
- dappTransactionModalVisible: state.modals.dappTransactionModalVisible,
- approveModalVisible: state.modals.approveModalVisible,
swapsTransactions:
state.engine.backgroundState.TransactionController.swapsTransactions || {},
providerType: selectProviderType(state),
@@ -840,9 +857,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setEtherTransaction(transaction)),
setTransactionObject: (transaction) =>
dispatch(setTransactionObject(transaction)),
- toggleDappTransactionModal: (show = null) =>
- dispatch(toggleDappTransactionModal(show)),
- toggleApproveModal: (show) => dispatch(toggleApproveModal(show)),
networkSwitched: ({ networkUrl, networkStatus }) =>
dispatch(networkSwitched({ networkUrl, networkStatus })),
});
diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx
index 33988971e66..d21f3ac47f3 100644
--- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx
+++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx
@@ -7,6 +7,20 @@ import renderWithProvider from '../../../util/test/renderWithProvider';
import { ENSCache } from '../../../util/ENSUtils';
import { Transaction } from './AccountFromToInfoCard.types';
import AccountFromToInfoCard from '.';
+import Engine from '../../../core/Engine';
+
+jest.mock('../../../util/address', () => ({
+ ...jest.requireActual('../../../util/address'),
+ isQRHardwareAccount: () => false,
+}));
+
+jest.mock('../../../core/Engine', () => ({
+ context: {
+ TokensController: {
+ addToken: () => undefined,
+ },
+ },
+}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
@@ -60,7 +74,12 @@ const initialState = {
},
},
},
- TokenBalancesController: {},
+ TokenBalancesController: {
+ contractBalances: {
+ '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5',
+ },
+ },
+ TokensController: {},
PreferencesController: {
selectedAddress: '0x0',
identities: {
@@ -217,4 +236,60 @@ describe('AccountFromToInfoCard', () => {
expect(await queryByText('test1.eth')).toBeDefined();
expect(await queryByText('test3.eth')).toBeDefined();
});
+
+ describe('from account balance', () => {
+ const ERC20Transaction = {
+ assetType: 'ERC20',
+ data: '0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000003a98',
+ from: '0x1',
+ selectedAsset: {
+ address: '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95',
+ decimals: '4',
+ image: 'https://metamask.github.io/test-dapp/metamask-fox.svg',
+ isERC721: false,
+ symbol: 'TST',
+ },
+ to: '0x2f318c334780961fb129d2a6c30d0763d9a5c970',
+ transaction: {
+ data: '0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000003a98',
+ from: '0x1',
+ to: '0x2f318c334780961fb129d2a6c30d0763d9a5c970',
+ value: '3a98',
+ },
+ };
+ let mockGetERC20BalanceOf: any;
+ beforeEach(() => {
+ jest.useFakeTimers();
+ mockGetERC20BalanceOf = jest.fn().mockReturnValue(0x0186a0);
+ Engine.context.AssetsContractController = {
+ getERC20BalanceOf: mockGetERC20BalanceOf,
+ };
+ });
+
+ it('should render balance from AssetsContractController.getERC20BalanceOf if selectedAddress is different from fromAddress', () => {
+ const { findByText } = renderWithProvider(
+ ,
+ { state: initialState },
+ );
+ expect(mockGetERC20BalanceOf).toBeCalledTimes(1);
+ expect(findByText('10 TST')).toBeDefined();
+ });
+
+ it('should render balance from TokenBalancesController.contractBalances if selectedAddress is same as fromAddress', () => {
+ const transaction = {
+ ...ERC20Transaction,
+ from: '0x0',
+ transaction: {
+ ...ERC20Transaction.transaction,
+ from: '0x0',
+ },
+ };
+ const { findByText } = renderWithProvider(
+ ,
+ { state: initialState },
+ );
+ expect(mockGetERC20BalanceOf).toBeCalledTimes(0);
+ expect(findByText('0.0005 TST')).toBeDefined();
+ });
+ });
});
diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx
new file mode 100644
index 00000000000..a926df8751e
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx
@@ -0,0 +1,198 @@
+import React from 'react';
+// eslint-disable-next-line @typescript-eslint/no-shadow
+import { waitFor, within } from '@testing-library/react-native';
+import Engine from '../../../core/Engine';
+import renderWithProvider from '../../../util/test/renderWithProvider';
+import AccountSelectorList from './AccountSelectorList';
+import { useAccounts } from '../../../components/hooks/useAccounts';
+import { View } from 'react-native';
+import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds';
+
+const mockEngine = Engine;
+
+jest.unmock('react-redux');
+
+const BUSINESS_ACCOUNT = '0x1';
+const PERSONAL_ACCOUNT = '0x2';
+
+jest.mock('../../../core/Engine', () => ({
+ init: () => mockEngine.init({}),
+ context: {
+ KeyringController: {
+ state: {
+ keyrings: [
+ {
+ type: 'HD Key Tree',
+ index: 0,
+ accounts: [BUSINESS_ACCOUNT, PERSONAL_ACCOUNT],
+ },
+ ],
+ },
+ },
+ },
+}));
+
+const initialState = {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ network: '1',
+ providerConfig: {
+ ticker: 'ETH',
+ type: 'mainnet',
+ chainId: '1',
+ },
+ },
+ AccountTrackerController: {
+ accounts: {
+ [BUSINESS_ACCOUNT]: { balance: '0xDE0B6B3A7640000' },
+ [PERSONAL_ACCOUNT]: { balance: '0x1BC16D674EC80000' },
+ },
+ },
+ PreferencesController: {
+ isMultiAccountBalancesEnabled: true,
+ selectedAddress: BUSINESS_ACCOUNT,
+ identities: {
+ [BUSINESS_ACCOUNT]: {
+ address: BUSINESS_ACCOUNT,
+ name: 'Business Account',
+ },
+ [PERSONAL_ACCOUNT]: {
+ address: PERSONAL_ACCOUNT,
+ name: 'Personal Account',
+ },
+ },
+ },
+ CurrencyRateController: {
+ conversionRate: 3200,
+ currentCurrency: 'usd',
+ nativeCurrency: 'ETH',
+ },
+ },
+ },
+ settings: {
+ primaryCurrency: 'ETH',
+ },
+};
+
+const onSelectAccount = jest.fn();
+const onRemoveImportedAccount = jest.fn();
+
+const AccountSelectorListUseAccounts = () => {
+ const { accounts, ensByAccountAddress } = useAccounts();
+ return (
+
+ );
+};
+
+const RIGHT_ACCESSORY_TEST_ID = 'right-accessory';
+
+const AccountSelectorListRightAccessoryUseAccounts = () => {
+ const { accounts, ensByAccountAddress } = useAccounts();
+ return (
+ (
+ {`${address} - ${name}`}
+ )}
+ isSelectionDisabled
+ selectedAddresses={[]}
+ accounts={accounts}
+ ensByAccountAddress={ensByAccountAddress}
+ />
+ );
+};
+
+const renderComponent = (
+ state: any = {},
+ AccountSelectorListTest = AccountSelectorListUseAccounts,
+) => renderWithProvider(, { state });
+
+describe('AccountSelectorList', () => {
+ beforeEach(() => {
+ onSelectAccount.mockClear();
+ onRemoveImportedAccount.mockClear();
+ });
+
+ it('should render correctly', async () => {
+ const { toJSON } = renderComponent(initialState);
+ await waitFor(() => expect(toJSON()).toMatchSnapshot());
+ });
+
+ it('should render all accounts with balances', async () => {
+ const { queryByTestId, getAllByTestId, toJSON } =
+ renderComponent(initialState);
+
+ await waitFor(async () => {
+ const businessAccountItem = await queryByTestId(
+ `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`,
+ );
+ const personalAccountItem = await queryByTestId(
+ `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`,
+ );
+
+ expect(within(businessAccountItem).getByText(/1 ETH/)).toBeDefined();
+ expect(within(businessAccountItem).getByText(/\$3200/)).toBeDefined();
+
+ expect(within(personalAccountItem).getByText(/2 ETH/)).toBeDefined();
+ expect(within(personalAccountItem).getByText(/\$6400/)).toBeDefined();
+
+ const accounts = getAllByTestId(
+ new RegExp(`${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}`),
+ );
+ expect(accounts.length).toBe(2);
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+ });
+
+ it('should render all accounts but only the balance for selected account', async () => {
+ const { queryByTestId, getAllByTestId, toJSON } = renderComponent({
+ engine: {
+ ...initialState.engine,
+ backgroundState: {
+ ...initialState.engine.backgroundState,
+ PreferencesController: {
+ ...initialState.engine.backgroundState.PreferencesController,
+ isMultiAccountBalancesEnabled: false,
+ },
+ },
+ },
+ });
+
+ await waitFor(async () => {
+ const accounts = getAllByTestId(
+ new RegExp(`${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}`),
+ );
+ expect(accounts.length).toBe(1);
+
+ const businessAccountItem = await queryByTestId(
+ `${ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`,
+ );
+
+ expect(within(businessAccountItem).getByText(/1 ETH/)).toBeDefined();
+ expect(within(businessAccountItem).getByText(/\$3200/)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+ });
+
+ it('should render all accounts with right acessory', async () => {
+ const { getAllByTestId, toJSON } = renderComponent(
+ initialState,
+ AccountSelectorListRightAccessoryUseAccounts,
+ );
+
+ await waitFor(() => {
+ const rightAccessories = getAllByTestId(RIGHT_ACCESSORY_TEST_ID);
+ expect(rightAccessories.length).toBe(2);
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx
index 6c24602074d..9a845189e4f 100644
--- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx
+++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx
@@ -1,6 +1,6 @@
// Third party dependencies.
import React, { useCallback, useRef } from 'react';
-import { Alert, ListRenderItem, View } from 'react-native';
+import { Alert, ListRenderItem, Platform, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { useSelector } from 'react-redux';
import { KeyringTypes } from '@metamask/keyring-controller';
@@ -24,6 +24,8 @@ import { removeAccountsFromPermissions } from '../../../core/Permissions';
// Internal dependencies.
import { AccountSelectorListProps } from './AccountSelectorList.types';
import styleSheet from './AccountSelectorList.styles';
+import generateTestId from '../../../../wdio/utils/generateTestId';
+import { ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID } from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds.js';
const AccountSelectorList = ({
onSelectAccount,
@@ -65,8 +67,14 @@ const AccountSelectorList = ({
};
const renderAccountBalances = useCallback(
- ({ fiatBalance, tokens }: Assets) => (
-
+ ({ fiatBalance, tokens }: Assets, address: string) => (
+
{fiatBalance}
{tokens && }
@@ -187,7 +195,7 @@ const AccountSelectorList = ({
style={cellStyle}
>
{renderRightAccessory?.(address, accountName) ||
- (assets && renderAccountBalances(assets))}
+ (assets && renderAccountBalances(assets, address))}
);
},
diff --git a/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
new file mode 100644
index 00000000000..4e53589d6a8
--- /dev/null
+++ b/app/components/UI/AccountSelectorList/__snapshots__/AccountSelector.test.tsx.snap
@@ -0,0 +1,1340 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AccountSelectorList should render all accounts but only the balance for selected account 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Business Account
+
+
+ 0x1
+
+
+
+
+
+ $3200.00
+1 ETH
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Personal Account
+
+
+ 0x2
+
+
+
+
+
+
+
+
+`;
+
+exports[`AccountSelectorList should render all accounts with balances 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Business Account
+
+
+ 0x1
+
+
+
+
+
+ $3200.00
+1 ETH
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Personal Account
+
+
+ 0x2
+
+
+
+
+
+ $6400.00
+2 ETH
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`AccountSelectorList should render all accounts with right acessory 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Business Account
+
+
+ 0x1
+
+
+
+
+ 0x1 - Business Account
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Personal Account
+
+
+ 0x2
+
+
+
+
+ 0x2 - Personal Account
+
+
+
+
+
+
+
+
+`;
+
+exports[`AccountSelectorList should render correctly 1`] = `
+
+
+
+`;
diff --git a/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.tsx b/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.tsx
index 9edb4e7320a..bf2992ba330 100644
--- a/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.tsx
+++ b/app/components/UI/AddToAddressBookWrapper/AddToAddressBookWrapper.tsx
@@ -3,7 +3,12 @@ import { useSelector } from 'react-redux';
import { View, Platform, TextInput, TouchableOpacity } from 'react-native';
import generateTestId from '../../../../wdio/utils/generateTestId';
-import { ENTER_ALIAS_INPUT_BOX_ID } from '../../../../wdio/screen-objects/testIDs/Screens/AddressBook.testids';
+import {
+ ADDRESS_ALIAS_CANCEL_BUTTON_ID,
+ ADDRESS_ALIAS_SAVE_BUTTON_ID,
+ ADDRESS_ALIAS_TITLE_ID,
+ ENTER_ALIAS_INPUT_BOX_ID,
+} from '../../../../wdio/screen-objects/testIDs/Screens/AddressBook.testids';
import { strings } from '../../../../locales/i18n';
import Engine from '../../../core/Engine';
import { ADD_ADDRESS_MODAL_CONTAINER_ID } from '../../../constants/test-ids';
@@ -70,6 +75,8 @@ export const AddToAddressBookWrapper = ({
cancelButtonMode={'normal'}
confirmButtonMode={'confirm'}
confirmDisabled={!alias}
+ cancelTestID={ADDRESS_ALIAS_CANCEL_BUTTON_ID}
+ confirmTestID={ADDRESS_ALIAS_SAVE_BUTTON_ID}
>
-
+
{strings('address_book.add_to_address_book')}
diff --git a/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx b/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx
index 0dd95fdc7c8..8c4bf45d91d 100644
--- a/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx
+++ b/app/components/UI/ApproveTransactionHeader/ApproveTransactionHeader.tsx
@@ -11,18 +11,22 @@ import TagUrl from '../../../component-library/components/Tags/TagUrl';
import { useStyles } from '../../../component-library/hooks';
import { selectProviderConfig } from '../../../selectors/networkController';
import { renderAccountName, renderShortAddress } from '../../../util/address';
-import { getHost, getUrlObj } from '../../../util/browser';
+import {
+ getHost,
+ getUrlObj,
+ prefixUrlWithProtocol,
+} from '../../../util/browser';
import {
getNetworkImageSource,
getNetworkNameFromProvider,
} from '../../../util/networks';
import { WALLET_CONNECT_ORIGIN } from '../../../util/walletconnect';
+import useAddressBalance from '../../hooks/useAddressBalance/useAddressBalance';
import {
FAV_ICON_URL,
ORIGIN_DEEPLINK,
ORIGIN_QR_CODE,
} from './ApproveTransactionHeader.constants';
-import useAddressBalance from '../../hooks/useAddressBalance/useAddressBalance';
import stylesheet from './ApproveTransactionHeader.styles';
import { ApproveTransactionHeaderI } from './ApproveTransactionHeader.types';
@@ -101,7 +105,7 @@ const ApproveTransactionHeader = ({
origin.split(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN)[1],
).origin;
} else {
- title = getUrlObj(currentEnsName || url || origin).origin;
+ title = prefixUrlWithProtocol(currentEnsName || origin || url);
}
return title;
@@ -116,14 +120,14 @@ const ApproveTransactionHeader = ({
]);
const favIconUrl = useMemo(() => {
- let newUrl = url;
+ let newUrl = origin;
if (isOriginWalletConnect) {
newUrl = origin.split(WALLET_CONNECT_ORIGIN)[1];
} else if (isOriginMMSDKRemoteConn) {
newUrl = origin.split(AppConstants.MM_SDK.SDK_REMOTE_ORIGIN)[1];
}
return FAV_ICON_URL(getHost(newUrl));
- }, [origin, isOriginWalletConnect, isOriginMMSDKRemoteConn, url]);
+ }, [origin, isOriginWalletConnect, isOriginMMSDKRemoteConn]);
return (
diff --git a/app/components/UI/ApproveTransactionReview/index.js b/app/components/UI/ApproveTransactionReview/index.js
index c363cbee321..ecd50b53e6f 100644
--- a/app/components/UI/ApproveTransactionReview/index.js
+++ b/app/components/UI/ApproveTransactionReview/index.js
@@ -1,5 +1,10 @@
import React, { PureComponent } from 'react';
-import { View, TouchableOpacity, InteractionManager } from 'react-native';
+import {
+ View,
+ TouchableOpacity,
+ InteractionManager,
+ Linking,
+} from 'react-native';
import Eth from 'ethjs-query';
import ActionView from '../../UI/ActionView';
import PropTypes from 'prop-types';
@@ -17,7 +22,10 @@ import { strings } from '../../../../locales/i18n';
import { setTransactionObject } from '../../../actions/transaction';
import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller';
import { hexToBN } from '@metamask/controller-utils';
-import { fromTokenMinimalUnit } from '../../../util/number';
+import {
+ fromTokenMinimalUnit,
+ renderFromTokenMinimalUnit,
+} from '../../../util/number';
import {
getTicker,
getNormalizedTxState,
@@ -26,7 +34,13 @@ import {
decodeApproveData,
generateTxWithNewTokenAllowance,
minimumTokenAllowance,
+ generateApproveData,
} from '../../../util/transactions';
+import Avatar, {
+ AvatarSize,
+ AvatarVariants,
+} from '../../../component-library/components/Avatars/Avatar';
+import Identicon from '../../UI/Identicon';
import TransactionTypes from '../../../core/TransactionTypes';
import { showAlert } from '../../../actions/alert';
import Analytics from '../../../core/Analytics/Analytics';
@@ -42,8 +56,10 @@ import {
isTestNet,
isMultiLayerFeeNetwork,
fetchEstimatedMultiLayerL1Fee,
+ isMainnetByChainId,
} from '../../../util/networks';
-import EditPermission from './EditPermission';
+import CustomSpendCap from '../../../component-library/components-temp/CustomSpendCap';
+import IonicIcon from 'react-native-vector-icons/Ionicons';
import Logger from '../../../util/Logger';
import ButtonLink from '../../../component-library/components/Buttons/Button/variants/ButtonLink';
import { getTokenList } from '../../../reducers/tokens';
@@ -53,7 +69,6 @@ import { ThemeContext, mockTheme } from '../../../util/theme';
import withQRHardwareAwareness from '../QRHardware/withQRHardwareAwareness';
import QRSigningDetails from '../QRHardware/QRSigningDetails';
import Routes from '../../../constants/navigation/Routes';
-import formatNumber from '../../../util/formatNumber';
import createStyles from './styles';
import {
selectChainId,
@@ -70,6 +85,8 @@ import VerifyContractDetails from './VerifyContractDetails/VerifyContractDetails
import ShowBlockExplorer from './ShowBlockExplorer';
import { isNetworkBuyNativeTokenSupported } from '../FiatOnRampAggregator/utils';
import { getRampNetworks } from '../../../reducers/fiatOrders';
+import SkeletonText from '../FiatOnRampAggregator/components/SkeletonText';
+import InfoModal from '../../../components/UI/Swaps/components/InfoModal';
const { ORIGIN_DEEPLINK, ORIGIN_QR_CODE } = AppConstants.DEEPLINKS;
const POLLING_INTERVAL_ESTIMATED_L1_FEE = 30000;
@@ -236,26 +253,39 @@ class ApproveTransactionReview extends PureComponent {
* Boolean that indicates if the native token buy is supported
*/
isNativeTokenBuySupported: PropTypes.bool,
+ /**
+ * Function to update token allowance state in Approve component
+ */
+ updateTokenAllowanceState: PropTypes.func,
+ /**
+ * Token allowance state from Approve component
+ */
+ tokenAllowanceState: PropTypes.object,
+ /**
+ * Boolean that indicates gas estimated value is confirmed before approving
+ */
+ isGasEstimateStatusIn: PropTypes.bool,
};
state = {
viewData: false,
- editPermissionVisible: false,
host: undefined,
originalApproveAmount: undefined,
- customSpendAmount: null,
- spendLimitUnlimitedSelected: true,
spendLimitCustomValue: undefined,
ticker: getTicker(this.props.ticker),
viewDetails: false,
spenderAddress: '0x...',
transaction: this.props.transaction,
token: {},
+ isReadyToApprove: false,
+ tokenSpendValue: '',
+ showGasTooltip: false,
gasTransactionObject: {},
multiLayerL1FeeTotal: '0x0',
fetchingUpdateDone: false,
showBlockExplorerModal: false,
address: '',
+ isCustomSpendInputValid: false,
};
customSpendLimitInput = React.createRef();
@@ -292,10 +322,13 @@ class ApproveTransactionReview extends PureComponent {
componentDidMount = async () => {
const { chainId } = this.props;
const {
- transaction: { origin, to, data, from },
+ transaction: { origin, to, data, from, transaction },
+ setTransactionObject,
tokenList,
+ tokenAllowanceState,
} = this.props;
- const { AssetsContractController } = Engine.context;
+ const { AssetsContractController, TokenBalancesController } =
+ Engine.context;
let host;
@@ -307,12 +340,39 @@ class ApproveTransactionReview extends PureComponent {
host = getHost(origin);
}
+ let tokenSymbol,
+ tokenDecimals,
+ tokenName,
+ tokenStandard,
+ tokenBalance,
+ createdSpendCap;
+
const { spenderAddress, encodedAmount } = decodeApproveData(data);
const encodedValue = hexToBN(encodedAmount).toString();
- let tokenSymbol, tokenDecimals, tokenName, tokenStandard;
+ const erc20TokenBalance = await TokenBalancesController.getERC20BalanceOf(
+ to,
+ from,
+ );
+
const contract = tokenList[safeToChecksumAddress(to)];
- if (!contract) {
+
+ if (tokenAllowanceState) {
+ const {
+ tokenSymbol: symbol,
+ tokenDecimals: decimals,
+ tokenName: name,
+ tokenBalance: balance,
+ tokenStandard: standard,
+ isReadyToApprove,
+ } = tokenAllowanceState;
+ tokenSymbol = symbol;
+ tokenDecimals = decimals;
+ tokenName = name;
+ tokenBalance = balance;
+ tokenStandard = standard;
+ createdSpendCap = isReadyToApprove;
+ } else if (!contract) {
try {
const result = await getTokenDetails(to, from, encodedValue);
@@ -329,6 +389,11 @@ class ApproveTransactionReview extends PureComponent {
tokenDecimals = decimals;
tokenSymbol = symbol;
tokenStandard = standard;
+ tokenName = name;
+ tokenBalance = renderFromTokenMinimalUnit(
+ erc20TokenBalance,
+ decimals,
+ );
}
} catch (e) {
tokenSymbol = 'ERC20 Token';
@@ -347,6 +412,25 @@ class ApproveTransactionReview extends PureComponent {
const { name: method } = await getMethodData(data);
const minTokenAllowance = minimumTokenAllowance(tokenDecimals);
+ const approvalData = generateApproveData({
+ spender: spenderAddress,
+ value:
+ tokenStandard === ERC721 || tokenStandard === ERC1155
+ ? encodedValue
+ : '0',
+ });
+
+ setTransactionObject({
+ transaction: {
+ ...transaction,
+ data: approvalData,
+ },
+ });
+
+ const token = Object.values(tokenList).filter(
+ (token) => token.address === to,
+ );
+
this.setState(
{
host,
@@ -358,10 +442,16 @@ class ApproveTransactionReview extends PureComponent {
tokenName,
tokenValue: encodedValue,
tokenStandard,
+ tokenBalance,
+ tokenImage: token[0]?.iconUrl,
},
spenderAddress,
encodedAmount,
fetchingUpdateDone: true,
+ isReadyToApprove: createdSpendCap,
+ tokenSpendValue: tokenAllowanceState
+ ? tokenAllowanceState?.tokenSpendValue
+ : '',
spendLimitCustomValue: minTokenAllowance,
},
() => {
@@ -380,6 +470,32 @@ class ApproveTransactionReview extends PureComponent {
}
};
+ componentDidUpdate = (_, prevState) => {
+ const { transaction, setTransactionObject } = this.props;
+ const {
+ tokenSpendValue,
+ spenderAddress,
+ token: { tokenDecimals },
+ } = this.state;
+
+ if (prevState?.tokenSpendValue !== tokenSpendValue) {
+ const newApprovalTransaction = generateTxWithNewTokenAllowance(
+ tokenSpendValue || '0',
+ tokenDecimals,
+ spenderAddress,
+ transaction,
+ );
+
+ setTransactionObject({
+ ...newApprovalTransaction,
+ transaction: {
+ ...newApprovalTransaction.transaction,
+ data: newApprovalTransaction.data,
+ },
+ });
+ }
+ };
+
componentWillUnmount = async () => {
clearInterval(intervalIdForEstimatedL1Fee);
};
@@ -449,41 +565,6 @@ class ApproveTransactionReview extends PureComponent {
this.setState({ viewDetails: !viewDetails });
};
- toggleEditPermission = () => {
- const { editPermissionVisible } = this.state;
- !editPermissionVisible &&
- this.trackApproveEvent(
- MetaMetricsEvents.DAPP_APPROVE_SCREEN_EDIT_PERMISSION,
- );
- this.setState({ editPermissionVisible: !editPermissionVisible });
- };
-
- onPressSpendLimitUnlimitedSelected = () => {
- const {
- token: { tokenDecimals },
- } = this.state;
- const minTokenAllowance = minimumTokenAllowance(tokenDecimals);
- this.setState({
- spendLimitUnlimitedSelected: true,
- spendLimitCustomValue: minTokenAllowance,
- });
- };
-
- onPressSpendLimitCustomSelected = () => {
- this.setState({ spendLimitUnlimitedSelected: false });
- setTimeout(
- () =>
- this.customSpendLimitInput &&
- this.customSpendLimitInput.current &&
- this.customSpendLimitInput.current.focus(),
- 100,
- );
- };
-
- onSpendLimitCustomValueChange = (value) => {
- this.setState({ spendLimitCustomValue: value });
- };
-
copyContractAddress = async (address) => {
await ClipboardManager.setString(address);
this.props.showAlert({
@@ -499,82 +580,67 @@ class ApproveTransactionReview extends PureComponent {
};
edit = () => {
- const { onModeChange } = this.props;
- Analytics.trackEvent(MetaMetricsEvents.TRANSACTIONS_EDIT_TRANSACTION);
- onModeChange && onModeChange('edit');
- };
-
- onEditPermissionSetAmount = () => {
+ const { onModeChange, updateTokenAllowanceState } = this.props;
const {
- token: { tokenDecimals },
- spenderAddress,
- spendLimitUnlimitedSelected,
+ token: {
+ tokenName,
+ tokenStandard,
+ tokenSymbol,
+ tokenDecimals,
+ tokenBalance,
+ },
+ tokenSpendValue,
originalApproveAmount,
- spendLimitCustomValue,
- transaction,
} = this.state;
+ Analytics.trackEvent(MetaMetricsEvents.TRANSACTIONS_EDIT_TRANSACTION);
- try {
- const { setTransactionObject } = this.props;
- const newApprovalTransaction = generateTxWithNewTokenAllowance(
- spendLimitUnlimitedSelected
- ? originalApproveAmount
- : spendLimitCustomValue,
- tokenDecimals,
- spenderAddress,
- transaction,
- );
-
- const { encodedAmount } = decodeApproveData(newApprovalTransaction.data);
-
- const approveAmount = fromTokenMinimalUnit(
- hexToBN(encodedAmount),
- tokenDecimals,
- );
-
- this.setState({ customSpendAmount: approveAmount });
- setTransactionObject({
- ...newApprovalTransaction,
- transaction: {
- ...newApprovalTransaction.transaction,
- data: newApprovalTransaction.data,
- },
- });
- } catch (err) {
- Logger.log('Failed to setTransactionObject', err);
- }
- this.toggleEditPermission();
- AnalyticsV2.trackEvent(
- MetaMetricsEvents.APPROVAL_PERMISSION_UPDATED,
- this.getAnalyticsParams(),
- );
+ updateTokenAllowanceState({
+ tokenStandard,
+ isReadyToApprove: true,
+ tokenSpendValue,
+ tokenBalance,
+ tokenSymbol,
+ originalApproveAmount,
+ tokenDecimals,
+ tokenName,
+ });
+ onModeChange && onModeChange('edit');
};
- renderEditPermission = () => {
- const {
- host,
- spendLimitUnlimitedSelected,
- spendLimitCustomValue,
- originalApproveAmount,
- token: { tokenSymbol, tokenDecimals },
- } = this.state;
- const minimumSpendLimit = minimumTokenAllowance(tokenDecimals);
+ openLinkAboutGas = () =>
+ Linking.openURL(AppConstants.URLS.WHY_TRANSACTION_TAKE_TIME);
+ toggleGasTooltip = () =>
+ this.setState((state) => ({ showGasTooltip: !state.showGasTooltip }));
+
+ renderGasTooltip = () => {
+ const isMainnet = isMainnetByChainId(this.props.chainId);
return (
-
+
+ {strings('transaction.gas_education_1')}
+ {strings(
+ `transaction.gas_education_2${isMainnet ? '_ethereum' : ''}`,
+ )}{' '}
+ {strings('transaction.gas_education_3')}
+
+
+ {strings('transaction.gas_education_4')}
+
+
+
+ {strings('transaction.gas_education_learn_more')}
+
+
+
}
- onPressSpendLimitCustomSelected={this.onPressSpendLimitCustomSelected}
- toggleEditPermission={this.toggleEditPermission}
/>
);
};
@@ -584,20 +650,32 @@ class ApproveTransactionReview extends PureComponent {
return createStyles(colors);
};
+ goToSpendCap = () => this.setState({ isReadyToApprove: false });
+
+ customSpendInputValid = (value) => {
+ this.setState({ isCustomSpendInputValid: value });
+ };
+
renderDetails = () => {
const {
originalApproveAmount,
- customSpendAmount,
+ host,
+ multiLayerL1FeeTotal,
token: {
tokenStandard,
tokenSymbol,
tokenName,
tokenValue,
tokenDecimals,
+ tokenBalance,
+ tokenImage,
},
- multiLayerL1FeeTotal,
+ tokenSpendValue,
fetchingUpdateDone,
+ isReadyToApprove,
+ isCustomSpendInputValid,
} = this.state;
+
const {
primaryCurrency,
gasError,
@@ -622,6 +700,7 @@ class ApproveTransactionReview extends PureComponent {
providerRpcTarget,
frequentRpcList,
isNativeTokenBuySupported,
+ isGasEstimateStatusIn,
} = this.props;
const styles = this.getStyles();
const isTestNetwork = isTestNet(network);
@@ -648,6 +727,23 @@ class ApproveTransactionReview extends PureComponent {
tokenName || tokenSymbol || strings(`spend_limit_edition.nft`)
} (#${tokenValue})`;
+ const isFirstScreenERC20 = tokenStandard === ERC20 && !tokenSpendValue;
+
+ const isFinalScreenNonERC20 = isReadyToApprove || tokenStandard !== ERC20;
+
+ const shouldDisableConfirmButton =
+ !fetchingUpdateDone ||
+ isFirstScreenERC20 ||
+ Boolean(gasError) ||
+ transactionConfirmed ||
+ !isCustomSpendInputValid ||
+ (isFinalScreenNonERC20 && !isGasEstimateStatusIn);
+
+ const confirmText =
+ tokenStandard === ERC20 && !isReadyToApprove
+ ? strings('transaction.next')
+ : strings('transactions.approve');
+
return (
<>
@@ -655,10 +751,10 @@ class ApproveTransactionReview extends PureComponent {
{from && (
@@ -683,16 +779,41 @@ class ApproveTransactionReview extends PureComponent {
`spend_limit_edition.${
originIsDeeplink
? 'allow_to_address_access'
- : 'allow_to_access'
+ : isReadyToApprove
+ ? 'review_spend_cap'
+ : tokenStandard === ERC721 || tokenStandard === ERC1155
+ ? 'allow_to_access'
+ : 'set_spend_cap'
}`,
- )}{' '}
+ )}
+
+
{!fetchingUpdateDone && (
-
+
{strings('spend_limit_edition.token')}
)}
{tokenStandard === ERC20 && (
- {tokenSymbol}
+ <>
+ {tokenImage ? (
+
+ ) : (
+
+ )}
+
+ {tokenSymbol}
+
+ >
)}
{tokenStandard === ERC721 || tokenStandard === ERC1155 ? (
hasBlockExplorer ? (
@@ -711,47 +832,18 @@ class ApproveTransactionReview extends PureComponent {
{tokenLabel}
)
) : null}
-
-
- {tokenStandard !== ERC721 &&
- tokenStandard !== ERC1155 &&
- originalApproveAmount && (
-
-
- {` ${strings('spend_limit_edition.access_up_to')} `}
-
-
- {` ${
- customSpendAmount
- ? formatNumber(customSpendAmount)
- : originalApproveAmount &&
- formatNumber(originalApproveAmount)
- } ${tokenSymbol}`}
-
-
- )}
-
- {fetchingUpdateDone &&
- tokenStandard !== ERC721 &&
- tokenStandard !== ERC1155 && (
-
-
- {strings('spend_limit_edition.edit_permission')}
-
-
- )}
-
- {`${strings(
- `spend_limit_edition.${
- originIsDeeplink
- ? 'you_trust_this_address'
- : 'you_trust_this_site'
- }`,
- )}`}
-
+
+ {(tokenStandard === ERC721 || tokenStandard === ERC1155) && (
+
+ {`${strings(
+ `spend_limit_edition.${
+ originIsDeeplink
+ ? 'you_trust_this_address'
+ : 'you_trust_this_site'
+ }`,
+ )}`}
+
+ )}
-
-
+ {!tokenStandard ? (
+
+ ) : (
+ tokenStandard === ERC20 && (
+
+ this.setState({
+ tokenSpendValue: value.replace(/[^0-9.]/g, ''),
+ })
+ }
+ />
+ )
+ )}
+ {((tokenStandard === ERC20 && isReadyToApprove) ||
+ tokenStandard === ERC721 ||
+ tokenStandard === ERC1155) && (
+
+
+
+ )}
{gasError && (
{isTestNetwork || isNativeTokenBuySupported ? (
@@ -812,10 +930,17 @@ class ApproveTransactionReview extends PureComponent {
style={styles.actionTouchable}
onPress={this.toggleViewDetails}
>
-
+
- {strings('spend_limit_edition.view_details')}
+ {strings(
+ 'spend_limit_edition.view_transaction_details',
+ )}
+
)}
@@ -835,17 +960,12 @@ class ApproveTransactionReview extends PureComponent {
host,
method,
viewData,
- originalApproveAmount,
- spendLimitUnlimitedSelected,
- spendLimitCustomValue,
+ tokenSpendValue,
token: { tokenStandard, tokenSymbol, tokenValue, tokenName },
} = this.state;
const {
transaction: { to, data },
} = this.props;
- const allowance =
- (!spendLimitUnlimitedSelected && spendLimitCustomValue) ||
- originalApproveAmount;
return (
{
+ const {
+ isReadyToApprove,
+ token: { tokenStandard },
+ } = this.state;
const { onConfirm } = this.props;
- onConfirm && onConfirm();
+
+ if (tokenStandard === ERC20 && !isReadyToApprove) {
+ AnalyticsV2.trackEvent(
+ MetaMetricsEvents.APPROVAL_PERMISSION_UPDATED,
+ this.getAnalyticsParams(),
+ );
+ return this.setState({ isReadyToApprove: true });
+ }
+
+ return onConfirm && onConfirm();
};
goToFaucet = () => {
@@ -1009,8 +1142,7 @@ class ApproveTransactionReview extends PureComponent {
}
render = () => {
- const { viewDetails, editPermissionVisible, showBlockExplorerModal } =
- this.state;
+ const { viewDetails, showBlockExplorerModal } = this.state;
const { isSigningQRObject, shouldVerifyContractDetails } = this.props;
return (
@@ -1021,8 +1153,6 @@ class ApproveTransactionReview extends PureComponent {
? this.renderVerifyContractDetails()
: showBlockExplorerModal
? this.renderBlockExplorerView()
- : editPermissionVisible
- ? this.renderEditPermission()
: isSigningQRObject
? this.renderQRDetails()
: this.renderDetails()}
@@ -1032,12 +1162,9 @@ class ApproveTransactionReview extends PureComponent {
}
const mapStateToProps = (state) => ({
- accounts: state.engine.backgroundState.AccountTrackerController.accounts,
ticker: selectTicker(state),
frequentRpcList:
state.engine.backgroundState.PreferencesController.frequentRpcList,
- selectedAddress:
- state.engine.backgroundState.PreferencesController.selectedAddress,
provider: state.engine.backgroundState.NetworkController.provider,
transaction: getNormalizedTxState(state),
accountsLength: Object.keys(
diff --git a/app/components/UI/ApproveTransactionReview/styles.ts b/app/components/UI/ApproveTransactionReview/styles.ts
index f500d94b3c4..871f0e70f0f 100644
--- a/app/components/UI/ApproveTransactionReview/styles.ts
+++ b/app/components/UI/ApproveTransactionReview/styles.ts
@@ -1,5 +1,6 @@
-import { fontStyles } from '../../../styles/common';
import { StyleSheet } from 'react-native';
+
+import { fontStyles } from '../../../styles/common';
import Device from '../../../util/device';
const createStyles = (colors: any) =>
@@ -17,7 +18,7 @@ const createStyles = (colors: any) =>
textAlign: 'center',
color: colors.text.default,
lineHeight: 34,
- marginVertical: 8,
+ marginVertical: 3,
paddingHorizontal: 16,
},
tokenKey: {
@@ -41,27 +42,21 @@ const createStyles = (colors: any) =>
marginHorizontal: 14,
flexDirection: 'row',
},
- editPermissionText: {
- ...fontStyles.bold,
- color: colors.primary.default,
- fontSize: 12,
- lineHeight: 20,
- textAlign: 'center',
- marginVertical: 10,
- borderWidth: 1,
- borderRadius: 20,
- borderColor: colors.primary.default,
- paddingVertical: 8,
- paddingHorizontal: 16,
- },
viewDetailsText: {
...fontStyles.normal,
color: colors.primary.default,
fontSize: 12,
lineHeight: 16,
- marginTop: 8,
+ marginHorizontal: 4,
textAlign: 'center',
},
+ iconContainer: {
+ flexDirection: 'row',
+ marginTop: 8,
+ },
+ iconDropdown: {
+ color: colors.icon.alternative,
+ },
actionTouchable: {
flexDirection: 'column',
alignItems: 'center',
@@ -147,6 +142,23 @@ const createStyles = (colors: any) =>
textAlign: 'center',
fontSize: 15,
},
+ skeletalView: {
+ height: 50,
+ },
+ transactionWrapper: {
+ marginVertical: 10,
+ },
+ symbol: {
+ marginHorizontal: 5,
+ },
+ alignText: {
+ textAlign: 'center',
+ },
+ tokenContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
});
export default createStyles;
diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx
index 8f1d1b68b37..3defd894549 100644
--- a/app/components/UI/AssetOverview/AssetOverview.tsx
+++ b/app/components/UI/AssetOverview/AssetOverview.tsx
@@ -7,7 +7,10 @@ import React, { useCallback, useEffect } from 'react';
import { Platform, TouchableOpacity, View } from 'react-native';
import { RootStateOrAny, useDispatch, useSelector } from 'react-redux';
import { strings } from '../../../../locales/i18n';
-import { TOKEN_ASSET_OVERVIEW } from '../../../../wdio/screen-objects/testIDs/Screens/TokenOverviewScreen.testIds';
+import {
+ TOKEN_ASSET_OVERVIEW,
+ TOKEN_OVERVIEW_SEND_BUTTON,
+} from '../../../../wdio/screen-objects/testIDs/Screens/TokenOverviewScreen.testIds';
import generateTestId from '../../../../wdio/utils/generateTestId';
import { toggleReceiveModal } from '../../../actions/modals';
import { newAssetTransaction } from '../../../actions/transaction';
@@ -242,6 +245,7 @@ const AssetOverview: React.FC = ({
size={ButtonSize.Lg}
label={strings('asset_overview.send_button')}
onPress={onSend}
+ {...generateTestId(Platform, TOKEN_OVERVIEW_SEND_BUTTON)}
/>
diff --git a/app/components/UI/CollectibleDetectionModal/index.tsx b/app/components/UI/CollectibleDetectionModal/index.tsx
index 2484cb8f6b8..55b2aabd1b7 100644
--- a/app/components/UI/CollectibleDetectionModal/index.tsx
+++ b/app/components/UI/CollectibleDetectionModal/index.tsx
@@ -25,12 +25,9 @@ interface Props {
const CollectibleDetectionModal = ({ onDismiss, navigation }: Props) => {
const goToSecuritySettings = () => {
navigation.navigate('SettingsView', {
- screen: 'SettingsFlow',
+ screen: 'SecuritySettings',
params: {
- screen: 'SecuritySettings',
- params: {
- scrollToBottom: true,
- },
+ scrollToBottom: true,
},
});
};
diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js
index 5187d63d8ca..0bee75a2491 100644
--- a/app/components/UI/DrawerView/index.js
+++ b/app/components/UI/DrawerView/index.js
@@ -72,10 +72,7 @@ import {
import Routes from '../../../constants/navigation/Routes';
import { scale } from 'react-native-size-matters';
import generateTestId from '../../../../wdio/utils/generateTestId';
-import {
- DRAWER_VIEW_LOCK_TEXT_ID,
- DRAWER_VIEW_SETTINGS_TEXT_ID,
-} from '../../../../wdio/screen-objects/testIDs/Screens/DrawerView.testIds';
+import { DRAWER_VIEW_LOCK_TEXT_ID } from '../../../../wdio/screen-objects/testIDs/Screens/DrawerView.testIds';
import { selectTicker } from '../../../selectors/networkController';
import { createAccountSelectorNavDetails } from '../../Views/AccountSelector';
@@ -669,18 +666,6 @@ class DrawerView extends PureComponent {
this.trackEvent(MetaMetricsEvents.WALLET_OPENED);
};
- goToTransactionHistory = () => {
- this.props.navigation.navigate('TransactionsHome');
- this.hideDrawer();
- this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_TRANSACTION_HISTORY);
- };
-
- showSettings = async () => {
- this.props.navigation.navigate('SettingsView');
- this.hideDrawer();
- this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_SETTINGS);
- };
-
onPressLock = async () => {
const { passwordSet } = this.props;
await Authentication.lockApp();
@@ -841,18 +826,6 @@ class DrawerView extends PureComponent {
);
}
- getSelectedFeatherIcon(name, size) {
- const colors = this.context.colors || mockTheme.colors;
-
- return (
-
- );
- }
-
getSelectedMaterialIcon(name, size) {
const colors = this.context.colors || mockTheme.colors;
@@ -890,15 +863,6 @@ class DrawerView extends PureComponent {
blockExplorerName = getBlockExplorerName(blockExplorer);
}
return [
- [
- {
- name: strings('drawer.transaction_activity'),
- icon: this.getFeatherIcon('list'),
- selectedIcon: this.getSelectedFeatherIcon('list'),
- action: this.goToTransactionHistory,
- routeNames: ['TransactionsView'],
- },
- ],
[
{
name: strings('drawer.share_address'),
@@ -915,13 +879,6 @@ class DrawerView extends PureComponent {
},
],
[
- {
- name: strings('drawer.settings'),
- icon: this.getFeatherIcon('settings'),
- warning: strings('drawer.settings_warning_short'),
- action: this.showSettings,
- testID: DRAWER_VIEW_SETTINGS_TEXT_ID,
- },
{
name: strings('drawer.help'),
icon: this.getIcon('comments'),
diff --git a/app/components/UI/EditGasFee1559/index.js b/app/components/UI/EditGasFee1559/index.js
index a04db09b2ae..dd73b169c7a 100644
--- a/app/components/UI/EditGasFee1559/index.js
+++ b/app/components/UI/EditGasFee1559/index.js
@@ -713,7 +713,10 @@ const EditGasFee1559 = ({
diff --git a/app/components/UI/EditGasFee1559Update/index.tsx b/app/components/UI/EditGasFee1559Update/index.tsx
index dace2a5e15a..6aa2f59dab7 100644
--- a/app/components/UI/EditGasFee1559Update/index.tsx
+++ b/app/components/UI/EditGasFee1559Update/index.tsx
@@ -30,7 +30,7 @@ import createStyles from './styles';
import { EditGasFee1559UpdateProps, RenderInputProps } from './types';
import generateTestId from '../../../../wdio/utils/generateTestId';
import {
- EDIT_PRIOTIRY_SCREEN_TEST_ID,
+ EDIT_PRIORITY_SCREEN_TEST_ID,
MAX_PRIORITY_FEE_INPUT_TEST_ID,
} from '../../../../wdio/screen-objects/testIDs/Screens/EditGasFeeScreen.testids.js';
@@ -589,7 +589,7 @@ const EditGasFee1559Update = ({
@@ -669,7 +669,10 @@ const EditGasFee1559Update = ({
diff --git a/app/components/UI/MessageSign/index.js b/app/components/UI/MessageSign/index.js
index 8d13c3f0b44..2f36d2ea993 100644
--- a/app/components/UI/MessageSign/index.js
+++ b/app/components/UI/MessageSign/index.js
@@ -237,6 +237,7 @@ class MessageSign extends PureComponent {
type="ethSign"
showWarning
fromAddress={from}
+ testID={'eth-signature-request'}
>
{this.renderMessageText()}
diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js
index 7636ca0c059..7b39e7de6ca 100644
--- a/app/components/UI/Navbar/index.js
+++ b/app/components/UI/Navbar/index.js
@@ -28,11 +28,7 @@ import Device from '../../../util/device';
import PickerNetwork from '../../../component-library/components/Pickers/PickerNetwork';
import BrowserUrlBar from '../BrowserUrlBar';
import generateTestId from '../../../../wdio/utils/generateTestId';
-import {
- HAMBURGER_MENU_BUTTON,
- NAVBAR_NETWORK_BUTTON,
- WALLET_VIEW_BURGER_ICON_ID,
-} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
+import { NAVBAR_NETWORK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
import {
NAV_ANDROID_BACK_BUTTON,
NETWORK_BACK_ARROW_BUTTON_ID,
@@ -54,6 +50,7 @@ import {
IconSize,
} from '../../../component-library/components/Icons/Icon';
import { EDIT_BUTTON } from '../../../../wdio/screen-objects/testIDs/Common.testIds';
+import Icon from '../../../component-library/components/Icons/Icon/Icon';
const trackEvent = (event) => {
InteractionManager.runAfterInteractions(() => {
@@ -136,7 +133,7 @@ const metamask_fox = require('../../../images/fox.png'); // eslint-disable-line
export function getTransactionsNavbarOptions(
title,
themeColors,
- navigation,
+ _,
selectedAddress,
handleRightButtonPress,
) {
@@ -156,22 +153,9 @@ export function getTransactionsNavbarOptions(
},
});
- function handleLeftButtonPress() {
- return navigation?.pop();
- }
-
return {
headerTitle: () => ,
- headerLeft: () => (
-
-
- {strings('navigation.close')}
-
-
- ),
+ headerLeft: null,
headerRight: () => (
),
headerLeft: () => (
-
-
-
+ testID="fox-icon"
+ />
),
headerRight: () => (
{
...innerStyles,
};
};
+
+export const getSettingsNavigationOptions = (title, themeColors) => {
+ const innerStyles = StyleSheet.create({
+ headerStyle: {
+ backgroundColor: themeColors.background.default,
+ shadowColor: importedColors.transparent,
+ elevation: 0,
+ },
+ headerTitleStyle: {
+ fontSize: 20,
+ color: themeColors.text.default,
+ ...fontStyles.normal,
+ },
+ });
+ return {
+ headerLeft: null,
+ headerTitle: {title},
+ ...innerStyles,
+ };
+};
diff --git a/app/components/UI/NavbarTitle/index.js b/app/components/UI/NavbarTitle/index.js
index 907f97fbb22..da6c1e765f5 100644
--- a/app/components/UI/NavbarTitle/index.js
+++ b/app/components/UI/NavbarTitle/index.js
@@ -11,7 +11,6 @@ import {
} from 'react-native';
import { fontStyles, colors as importedColors } from '../../../styles/common';
import Networks from '../../../util/networks';
-import { toggleNetworkModal } from '../../../actions/modals';
import { strings } from '../../../../locales/i18n';
import Device from '../../../util/device';
import { ThemeContext, mockTheme } from '../../../util/theme';
@@ -70,10 +69,6 @@ class NavbarTitle extends PureComponent {
* Name of the current view
*/
title: PropTypes.string,
- /**
- * Action that toggles the network modal
- */
- toggleNetworkModal: PropTypes.func,
/**
* Boolean that specifies if the title needs translation
*/
@@ -173,9 +168,5 @@ NavbarTitle.contextType = ThemeContext;
const mapStateToProps = (state) => ({
network: state.engine.backgroundState.NetworkController,
});
-const mapDispatchToProps = (dispatch) => ({
- toggleNetworkModal: () => dispatch(toggleNetworkModal()),
-});
-export default withNavigation(
- connect(mapStateToProps, mapDispatchToProps)(NavbarTitle),
-);
+
+export default withNavigation(connect(mapStateToProps)(NavbarTitle));
diff --git a/app/components/UI/NetworkInfo/InfoDescription/infoDescription.tsx b/app/components/UI/NetworkInfo/InfoDescription/infoDescription.tsx
index b67bf9a0db7..ccf317dd2bc 100644
--- a/app/components/UI/NetworkInfo/InfoDescription/infoDescription.tsx
+++ b/app/components/UI/NetworkInfo/InfoDescription/infoDescription.tsx
@@ -66,13 +66,10 @@ const Description = (props: DescriptionProps) => {
const handleNavigation = useCallback(() => {
onClose?.();
navigation.navigate('SettingsView', {
- screen: 'SettingsFlow',
+ screen: 'AdvancedSettings',
params: {
- screen: 'AdvancedSettings',
- params: {
- scrollToBottom: true,
- isFullScreenModal: true,
- },
+ scrollToBottom: true,
+ isFullScreenModal: true,
},
});
}, [navigation, onClose]);
diff --git a/app/components/UI/NetworkModal/index.tsx b/app/components/UI/NetworkModal/index.tsx
index 360da92abea..84eedfd1c41 100644
--- a/app/components/UI/NetworkModal/index.tsx
+++ b/app/components/UI/NetworkModal/index.tsx
@@ -146,43 +146,9 @@ const NetworkModals = (props: NetworkProps) => {
};
const addNetwork = async () => {
- const { PreferencesController } = Engine.context;
- let formChainId = chainId.trim().toLowerCase();
-
- if (!formChainId.startsWith('0x')) {
- formChainId = `0x${parseInt(formChainId, 10).toString(16)}`;
- }
-
const validUrl = validateRpcUrl(rpcUrl);
- if (validUrl) {
- const url = new URLPARSE(rpcUrl);
- const decimalChainId = getDecimalChainId(chainId);
- !isprivateConnection(url.hostname) && url.set('protocol', 'https:');
- PreferencesController.addToFrequentRpcList(
- url.href,
- decimalChainId,
- ticker,
- nickname,
- {
- blockExplorerUrl,
- },
- );
-
- const analyticsParamsAdd = {
- chain_id: decimalChainId,
- source: 'Popular network list',
- symbol: ticker,
- };
-
- AnalyticsV2.trackEvent(
- MetaMetricsEvents.NETWORK_ADDED,
- analyticsParamsAdd,
- );
- setNetworkAdded(true);
- } else {
- setNetworkAdded(false);
- }
+ setNetworkAdded(validUrl);
};
const showToolTip = () => setShowInfo(!showInfo);
@@ -190,14 +156,47 @@ const NetworkModals = (props: NetworkProps) => {
const goToLink = () => Linking.openURL(strings('networks.security_link'));
const closeModal = () => {
+ const { PreferencesController } = Engine.context;
+ const url = new URLPARSE(rpcUrl);
+ const decimalChainId = getDecimalChainId(chainId);
+ !isprivateConnection(url.hostname) && url.set('protocol', 'https:');
+ PreferencesController.addToFrequentRpcList(
+ url.href,
+ decimalChainId,
+ ticker,
+ nickname,
+ {
+ blockExplorerUrl,
+ },
+ );
onClose();
};
const switchNetwork = () => {
- const { NetworkController, CurrencyRateController } = Engine.context;
+ const { NetworkController, CurrencyRateController, PreferencesController } =
+ Engine.context;
const url = new URLPARSE(rpcUrl);
const decimalChainId = getDecimalChainId(chainId);
CurrencyRateController.setNativeCurrency(ticker);
+ !isprivateConnection(url.hostname) && url.set('protocol', 'https:');
+ PreferencesController.addToFrequentRpcList(
+ url.href,
+ decimalChainId,
+ ticker,
+ nickname,
+ {
+ blockExplorerUrl,
+ },
+ );
+
+ const analyticsParamsAdd = {
+ chain_id: decimalChainId,
+ source: 'Popular network list',
+ symbol: ticker,
+ };
+
+ AnalyticsV2.trackEvent(MetaMetricsEvents.NETWORK_ADDED, analyticsParamsAdd);
+
NetworkController.setRpcTarget(url.href, decimalChainId, ticker, nickname);
closeModal();
shouldNetworkSwitchPopToWallet
diff --git a/app/components/UI/OnboardingWizard/Coachmark/index.js b/app/components/UI/OnboardingWizard/Coachmark/index.js
index 6f44486b12d..afb104d6571 100644
--- a/app/components/UI/OnboardingWizard/Coachmark/index.js
+++ b/app/components/UI/OnboardingWizard/Coachmark/index.js
@@ -136,11 +136,17 @@ const createStyles = (colors) =>
alignItems: 'flex-start',
marginLeft: 60,
},
+ bottomLeftCorner: {
+ marginBottom: 10,
+ top: -2,
+ alignItems: 'flex-start',
+ marginLeft: 30,
+ },
bottomRight: {
marginBottom: 10,
top: -2,
alignItems: 'flex-end',
- marginRight: 30,
+ marginRight: 90,
},
circle: {
width: 6,
@@ -217,6 +223,7 @@ export default class Coachmark extends PureComponent {
false,
'bottomCenter',
'bottomLeft',
+ 'bottomLeftCorner',
'bottomRight',
]),
/**
@@ -302,6 +309,7 @@ export default class Coachmark extends PureComponent {
const positions = {
bottomCenter: styles.bottomCenter,
bottomLeft: styles.bottomLeft,
+ bottomLeftCorner: styles.bottomLeftCorner,
bottomRight: styles.bottomRight,
[undefined]: styles.bottomCenter,
};
diff --git a/app/components/UI/OnboardingWizard/Step1/index.js b/app/components/UI/OnboardingWizard/Step1/index.js
index bcd245aac54..00378bdd4db 100644
--- a/app/components/UI/OnboardingWizard/Step1/index.js
+++ b/app/components/UI/OnboardingWizard/Step1/index.js
@@ -102,7 +102,7 @@ class Step1 extends PureComponent {
onNext={this.onNext}
onBack={this.onClose}
coachmarkStyle={styles.coachmark}
- bottomIndicatorPosition={'bottomLeft'}
+ bottomIndicatorPosition={'bottomLeftCorner'}
action
onClose={this.onClose}
/>
diff --git a/app/components/UI/OptinMetrics/index.js b/app/components/UI/OptinMetrics/index.js
index b5c71ca0591..f3cc890338e 100644
--- a/app/components/UI/OptinMetrics/index.js
+++ b/app/components/UI/OptinMetrics/index.js
@@ -44,6 +44,7 @@ import Button, {
ButtonSize,
} from '../../../component-library/components/Buttons/Button';
import { MAINNET } from '../../../constants/network';
+import Routes from '../../../constants/navigation/Routes';
const createStyles = ({ colors }) =>
StyleSheet.create({
@@ -312,7 +313,7 @@ class OptinMetrics extends PureComponent {
* Open RPC settings.
*/
openRPCSettings = () => {
- this.props.navigation.navigate('NetworkSettings', {
+ this.props.navigation.navigate(Routes.ADD_NETWORK, {
network: MAINNET,
isCustomMainnet: true,
});
diff --git a/app/components/UI/PersonalSign/PersonalSign.tsx b/app/components/UI/PersonalSign/PersonalSign.tsx
index 9eb6543768b..3b2ae7b15fe 100644
--- a/app/components/UI/PersonalSign/PersonalSign.tsx
+++ b/app/components/UI/PersonalSign/PersonalSign.tsx
@@ -192,6 +192,7 @@ const PersonalSign = ({
truncateMessage={truncateMessage}
type="personalSign"
fromAddress={messageParams.from}
+ testID={'personal-signature-request'}
>
{renderMessageText()}
diff --git a/app/components/UI/SearchTokenAutocomplete/index.tsx b/app/components/UI/SearchTokenAutocomplete/index.tsx
index 1353814fdef..f472ddd04d0 100644
--- a/app/components/UI/SearchTokenAutocomplete/index.tsx
+++ b/app/components/UI/SearchTokenAutocomplete/index.tsx
@@ -168,12 +168,9 @@ const SearchTokenAutocomplete = ({ navigation }: Props) => {
suppressHighlighting
onPress={() => {
navigation.navigate('SettingsView', {
- screen: 'SettingsFlow',
+ screen: 'AdvancedSettings',
params: {
- screen: 'AdvancedSettings',
- params: {
- scrollToBottom: true,
- },
+ scrollToBottom: true,
},
});
}}
diff --git a/app/components/UI/SettingsDrawer/index.js b/app/components/UI/SettingsDrawer/index.js
index e5c772eda35..9b08fe24fe0 100644
--- a/app/components/UI/SettingsDrawer/index.js
+++ b/app/components/UI/SettingsDrawer/index.js
@@ -1,13 +1,20 @@
import React from 'react';
-import { Text, View, StyleSheet, TouchableOpacity } from 'react-native';
+import {
+ Text,
+ View,
+ StyleSheet,
+ TouchableOpacity,
+ Platform,
+} from 'react-native';
import PropTypes from 'prop-types';
import { fontStyles } from '../../../styles/common';
import Icon from 'react-native-vector-icons/FontAwesome';
import SettingsNotification from '../SettingsNotification';
import { strings } from '../../../../locales/i18n';
import { useTheme } from '../../../util/theme';
+import generateTestId from '../../../../wdio/utils/generateTestId';
-const createStyles = (colors) =>
+const createStyles = (colors, titleColor) =>
StyleSheet.create({
root: {
backgroundColor: colors.background.default,
@@ -19,10 +26,11 @@ const createStyles = (colors) =>
},
content: {
flex: 1,
+ justifyContent: 'center',
},
title: {
...fontStyles.normal,
- color: colors.text.default,
+ color: titleColor || colors.text.default,
fontSize: 20,
marginBottom: 8,
},
@@ -36,6 +44,7 @@ const createStyles = (colors) =>
action: {
flex: 0,
paddingHorizontal: 16,
+ justifyContent: 'center',
},
icon: {
bottom: 8,
@@ -75,22 +84,43 @@ const propTypes = {
* Display SettingsNotification
*/
warning: PropTypes.bool,
+ /**
+ * Display arrow right
+ */
+ renderArrowRight: PropTypes.bool,
+ /**
+ * Test id for testing purposes
+ */
+ testID: PropTypes.string,
+ /**
+ * Title color
+ */
+ titleColor: PropTypes.string,
};
const defaultProps = {
onPress: undefined,
};
-const SettingsDrawer = ({ title, description, noBorder, onPress, warning }) => {
+const SettingsDrawer = ({
+ title,
+ description,
+ noBorder,
+ onPress,
+ warning,
+ renderArrowRight = true,
+ testID,
+ titleColor,
+}) => {
const { colors } = useTheme();
- const styles = createStyles(colors);
+ const styles = createStyles(colors, titleColor);
return (
-
+
{title}
- {description}
+ {description && {description}}
{warning ? (
{
) : null}
-
-
-
+ {renderArrowRight && (
+
+
+
+ )}
);
diff --git a/app/components/UI/SignatureRequest/Root/__snapshots__/Root.test.tsx.snap b/app/components/UI/SignatureRequest/Root/__snapshots__/Root.test.tsx.snap
index 99658b2f506..595057dd660 100644
--- a/app/components/UI/SignatureRequest/Root/__snapshots__/Root.test.tsx.snap
+++ b/app/components/UI/SignatureRequest/Root/__snapshots__/Root.test.tsx.snap
@@ -121,6 +121,7 @@ exports[`Root should match snapshot 1`] = `
undefined,
]
}
+ testID="personal-signature-request"
>
+
(
? strings('times_eip1559.warning_low_title')
: timeEstimateId === AppConstants.GAS_TIMES.UNKNOWN
? strings('times_eip1559.warning_unknown_title')
+ : timeEstimateId === AppConstants.GAS_TIMES.VERY_LIKELY
+ ? strings('times_eip1559.warning_very_likely_title')
: null
}
body={
@@ -24,6 +26,8 @@ const TimeEstimateInfoModal = ({ timeEstimateId, isVisible, onHideModal }) => (
strings('times_eip1559.warning_unknown')}
{timeEstimateId === AppConstants.GAS_TIMES.MAYBE &&
strings('times_eip1559.warning_low')}
+ {timeEstimateId === AppConstants.GAS_TIMES.VERY_LIKELY &&
+ strings('times_eip1559.warning_very_likely')}
}
diff --git a/app/components/UI/Tokens/index.tsx b/app/components/UI/Tokens/index.tsx
index 6e33b451c6e..dca7bceb61e 100644
--- a/app/components/UI/Tokens/index.tsx
+++ b/app/components/UI/Tokens/index.tsx
@@ -30,7 +30,9 @@ import { useTheme } from '../../../util/theme';
import NotificationManager from '../../../core/NotificationManager';
import {
getDecimalChainId,
+ getNetworkNameFromProvider,
getTestNetImageByChainId,
+ isLineaMainnetByChainId,
isMainnetByChainId,
isTestNet,
} from '../../../util/networks';
@@ -41,7 +43,7 @@ import {
} from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
import {
selectChainId,
- selectProviderType,
+ selectProviderConfig,
selectTicker,
} from '../../../selectors/networkController';
import { createDetectedTokensNavDetails } from '../../Views/DetectedTokens';
@@ -96,7 +98,10 @@ const Tokens: React.FC = ({ tokens }) => {
const actionSheet = useRef();
- const providerType = useSelector(selectProviderType);
+ const networkName = useSelector((state: EngineState) => {
+ const providerConfig = selectProviderConfig(state);
+ return getNetworkNameFromProvider(providerConfig);
+ });
const chainId = useSelector(selectChainId);
const ticker = useSelector(selectTicker);
const currentCurrency = useSelector(
@@ -257,17 +262,18 @@ const Tokens: React.FC = ({ tokens }) => {
asset = { ...asset, balanceFiat };
const isMainnet = isMainnetByChainId(chainId);
+ const isLineaMainnet = isLineaMainnetByChainId(chainId);
const NetworkBadgeSource = () => {
if (isTestNet(chainId)) return getTestNetImageByChainId(chainId);
if (isMainnet) return images.ETHEREUM;
+ if (isLineaMainnet) return images['LINEA-MAINNET'];
+
return images[ticker];
};
- const badgeName = (isMainnet ? providerType : ticker) || '';
-
return (
= ({ tokens }) => {
}
>
diff --git a/app/components/UI/TransactionElement/utils.js b/app/components/UI/TransactionElement/utils.js
index 40cc443086e..af5988600d8 100644
--- a/app/components/UI/TransactionElement/utils.js
+++ b/app/components/UI/TransactionElement/utils.js
@@ -33,8 +33,7 @@ import { swapsUtils } from '@metamask/swaps-controller';
import { isSwapsNativeAsset } from '../Swaps/utils';
import { toLowerCaseEquals } from '../../../util/general';
import Engine from '../../../core/Engine';
-// TODO: Update after this function has been exported from the package
-import { isEIP1559Transaction } from '@metamask/transaction-controller/dist/utils';
+import { isEIP1559Transaction } from '@metamask/transaction-controller';
const { getSwapsContractAddress } = swapsUtils;
diff --git a/app/components/UI/TransactionReview/TransactionReviewDetailsCard/index.js b/app/components/UI/TransactionReview/TransactionReviewDetailsCard/index.js
index dcdfadaaeec..333a7a80ab6 100644
--- a/app/components/UI/TransactionReview/TransactionReviewDetailsCard/index.js
+++ b/app/components/UI/TransactionReview/TransactionReviewDetailsCard/index.js
@@ -95,7 +95,7 @@ export default class TransactionReviewDetailsCard extends Component {
toggleViewData: PropTypes.func,
address: PropTypes.string,
host: PropTypes.string,
- allowance: PropTypes.string,
+ tokenSpendValue: PropTypes.string,
tokenSymbol: PropTypes.string,
data: PropTypes.string,
displayViewData: PropTypes.bool,
@@ -114,7 +114,7 @@ export default class TransactionReviewDetailsCard extends Component {
copyContractAddress,
address,
host,
- allowance,
+ tokenSpendValue,
tokenSymbol,
data,
method,
@@ -174,7 +174,7 @@ export default class TransactionReviewDetailsCard extends Component {
{tokenStandard === ERC20
- ? `${formatNumber(allowance)} ${tokenSymbol}`
+ ? `${formatNumber(tokenSpendValue || '0')} ${tokenSymbol}`
: `${tokenName} (#${tokenValue})`}
diff --git a/app/components/UI/TransactionReview/TransactionReviewEIP1559/__snapshots__/index.test.tsx.snap b/app/components/UI/TransactionReview/TransactionReviewEIP1559/__snapshots__/index.test.tsx.snap
index fc57209ae84..88848885d4b 100644
--- a/app/components/UI/TransactionReview/TransactionReviewEIP1559/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/TransactionReview/TransactionReviewEIP1559/__snapshots__/index.test.tsx.snap
@@ -1,37 +1,1350 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`TransactionReviewEIP1559 should render correctly 1`] = `
-
-
-
+
+
+
+
+ Estimated gas fee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This gas fee suggestion is using legacy gas estimation which may be inaccurate.
+
+
+
+
+
+
+
+
+
+
+
+ What are gas fees?
+
+
+
+
+
+
+
+
+
+
+ Gas fees are paid to crypto miners who process transactions on the
+ Ethereum
+ network.
+
+
+ MetaMask does not profit from gas fees.
+
+
+
+ Gas fees are set by the network and fluctuate based on network traffic and transaction complexity.
+
+
+
+ Learn more about gas fees
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.js b/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.js
index 1c9d7d1655e..600c8a89cb2 100644
--- a/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.js
+++ b/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.js
@@ -259,23 +259,24 @@ const TransactionReviewEIP1559 = ({
small
green={timeEstimateColor === 'green'}
red={timeEstimateColor === 'red'}
+ orange={timeEstimateColor === 'orange'}
>
{timeEstimate}
+ {(timeEstimateId === AppConstants.GAS_TIMES.MAYBE ||
+ timeEstimateId === AppConstants.GAS_TIMES.UNKNOWN) && (
+
+
+
+ )}
- {(timeEstimateId === AppConstants.GAS_TIMES.MAYBE ||
- timeEstimateId === AppConstants.GAS_TIMES.UNKNOWN) && (
-
-
-
- )}
) : (
@@ -287,11 +288,43 @@ const TransactionReviewEIP1559 = ({
valueToWatch={valueToWatchAnimation}
animateOnChange={animateOnChange}
>
-
-
+
+
+ {timeEstimateId === AppConstants.GAS_TIMES.VERY_LIKELY && (
+
+
+
+ )}
+ {' '}
+
{strings('transaction_review_eip1559.max_fee')}:{' '}
-
+
{gasFeeMaxPrimary}
diff --git a/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.test.tsx b/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.test.tsx
index 6aa558c0e9b..6b98c15766e 100644
--- a/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.test.tsx
+++ b/app/components/UI/TransactionReview/TransactionReviewEIP1559/index.test.tsx
@@ -1,10 +1,7 @@
import React from 'react';
import TransactionReviewEIP1559 from './';
-import { shallow } from 'enzyme';
-import configureMockStore from 'redux-mock-store';
-import { Provider } from 'react-redux';
+import renderWithProvider from '../../../..//util/test/renderWithProvider';
-const mockStore = configureMockStore();
const initialState = {
engine: {
backgroundState: {
@@ -21,15 +18,12 @@ const initialState = {
},
},
};
-const store = mockStore(initialState);
describe('TransactionReviewEIP1559', () => {
- it('should render correctly', () => {
- const wrapper = shallow(
-
-
- ,
- );
- expect(wrapper).toMatchSnapshot();
+ it('should match snapshot', async () => {
+ const container = renderWithProvider(, {
+ state: initialState,
+ });
+ expect(container).toMatchSnapshot();
});
});
diff --git a/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/__snapshots__/index.test.tsx.snap b/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/__snapshots__/index.test.tsx.snap
new file mode 100644
index 00000000000..d44c1c90c38
--- /dev/null
+++ b/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,1338 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TransactionReviewEIP1559 should render correctly 1`] = `
+
+
+
+
+
+ Estimated gas fee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This gas fee suggestion is using legacy gas estimation which may be inaccurate.
+
+
+
+
+
+
+
+
+
+
+
+ What are gas fees?
+
+
+
+
+
+
+
+
+
+
+ Gas fees are paid to crypto miners who process transactions on the
+ Ethereum
+ network.
+
+
+ MetaMask does not profit from gas fees.
+
+
+
+ Gas fees are set by the network and fluctuate based on network traffic and transaction complexity.
+
+
+
+ Learn more about gas fees
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/index.test.tsx b/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/index.test.tsx
new file mode 100644
index 00000000000..7f09bfdbcc7
--- /dev/null
+++ b/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/index.test.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+
+import renderWithProvider, {
+ renderHookWithProvider,
+} from '../../../../util/test/renderWithProvider';
+import TransactionReviewEIP1559 from './';
+
+const initialState = {
+ settings: {},
+ engine: {
+ backgroundState: {
+ AccountTrackerController: {
+ accounts: {
+ '0x0': {
+ balance: 200,
+ },
+ },
+ },
+ GasFeeController: {
+ gasFeeEstimates: {
+ low: '0x0',
+ medium: '0x0',
+ high: '0x0',
+ },
+ gasEstimateType: 'low',
+ },
+ TokenRatesController: {
+ contractExchangeRates: {
+ '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': 1,
+ },
+ },
+ TokenBalancesController: {
+ contractBalances: {
+ '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5',
+ },
+ },
+ CurrencyRateController: {
+ conversionRate: 10,
+ currentCurrency: 'usd',
+ },
+ },
+ },
+};
+
+const transactionReview = {
+ primaryCurrency: 'USD',
+ chainId: '1',
+ onEdit: () => undefined,
+ hideTotal: false,
+ noMargin: false,
+ origin: '',
+ originWarning: '',
+ onUpdatingValuesStart: () => undefined,
+ onUpdatingValuesEnd: () => undefined,
+ animateOnChange: false,
+ isAnimating: false,
+ gasEstimationReady: false,
+ legacy: false,
+ gasSelected: '',
+ gasObject: {
+ suggestedMaxFeePerGas: '',
+ suggestedMaxPriorityFeePerGas: '',
+ },
+ updateTransactionState: undefined,
+ onlyGas: false,
+};
+
+describe('TransactionReviewEIP1559', () => {
+ it('should render correctly', () => {
+ const wrapper = renderWithProvider(
+ ,
+ { state: initialState },
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should call gasTransaction if gasEstimationReady is true', () => {
+ const updateTransactionStateMock = jest.fn();
+
+ renderHookWithProvider(
+ () =>
+ TransactionReviewEIP1559({
+ ...transactionReview,
+ gasEstimationReady: true,
+ updateTransactionState: updateTransactionStateMock,
+ }),
+ {
+ state: initialState,
+ },
+ );
+
+ expect(updateTransactionStateMock).toHaveBeenCalled();
+ });
+});
diff --git a/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/index.tsx b/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/index.tsx
index d923042f967..90394c83856 100644
--- a/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/index.tsx
+++ b/app/components/UI/TransactionReview/TransactionReviewEIP1559Update/index.tsx
@@ -1,23 +1,24 @@
-import React, { useState, useCallback, useEffect } from 'react';
-import { TouchableOpacity, View, Linking, Platform } from 'react-native';
-import Summary from '../../../Base/Summary';
-import Text from '../../../Base/Text';
+import React, { useCallback, useEffect, useState } from 'react';
+import { Linking, Platform, TouchableOpacity, View } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
-import { isMainnetByChainId } from '../../../../util/networks';
-import InfoModal from '../../Swaps/components/InfoModal';
-import FadeAnimationView from '../../FadeAnimationView';
+
import { strings } from '../../../../../locales/i18n';
-import TimeEstimateInfoModal from '../../TimeEstimateInfoModal';
-import useModalHandler from '../../../Base/hooks/useModalHandler';
+import { ESTIMATED_FEE_TEST_ID } from '../../../../../wdio/screen-objects/testIDs/Screens/TransactionSummaryScreen.testIds';
+import generateTestId from '../../../../../wdio/utils/generateTestId';
import AppConstants from '../../../../core/AppConstants';
-import Device from '../../../../util/device';
-import { useAppThemeFromContext, mockTheme } from '../../../../util/theme';
import { useGasTransaction } from '../../../../core/GasPolling/GasPolling';
+import Device from '../../../../util/device';
+import { isMainnetByChainId } from '../../../../util/networks';
+import { mockTheme, useAppThemeFromContext } from '../../../../util/theme';
+import useModalHandler from '../../../Base/hooks/useModalHandler';
+import Summary from '../../../Base/Summary';
+import Text from '../../../Base/Text';
+import FadeAnimationView from '../../FadeAnimationView';
+import InfoModal from '../../Swaps/components/InfoModal';
+import TimeEstimateInfoModal from '../../TimeEstimateInfoModal';
+import SkeletonComponent from './skeletonComponent';
import createStyles from './styles';
import { TransactionEIP1559UpdateProps } from './types';
-import SkeletonComponent from './skeletonComponent';
-import generateTestId from '../../../../../wdio/utils/generateTestId';
-import { ESTIMATED_FEE_TEST_ID } from '../../../../../wdio/screen-objects/testIDs/Screens/TransactionSummaryScreen.testIds';
const TransactionReviewEIP1559Update = ({
primaryCurrency,
@@ -79,10 +80,11 @@ const TransactionReviewEIP1559Update = ({
} = gasTransaction;
useEffect(() => {
- if (animateOnChange) {
+ if (gasEstimationReady) {
updateTransactionState(gasTransaction);
}
- }, [animateOnChange, gasTransaction, updateTransactionState]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [gasEstimationReady, updateTransactionState]);
const openLinkAboutGas = useCallback(
() => Linking.openURL(AppConstants.URLS.WHY_TRANSACTION_TAKE_TIME),
@@ -224,6 +226,7 @@ const TransactionReviewEIP1559Update = ({
small
green={timeEstimateColor === 'green'}
red={timeEstimateColor === 'red'}
+ orange={timeEstimateColor === 'orange'}
>
{timeEstimate}
@@ -252,11 +255,43 @@ const TransactionReviewEIP1559Update = ({
valueToWatch={valueToWatchAnimation}
animateOnChange={animateOnChange}
>
-
-
+
+
+ {timeEstimateId === AppConstants.GAS_TIMES.VERY_LIKELY && (
+
+
+
+ )}
+ {' '}
+
{strings('transaction_review_eip1559.max_fee')}:{' '}
-
+
{switchNativeCurrencyDisplayOptions(
renderableGasFeeMaxNative,
renderableGasFeeMaxConversion,
diff --git a/app/components/UI/TypedSign/index.js b/app/components/UI/TypedSign/index.js
index 8f84adf7855..12327041475 100644
--- a/app/components/UI/TypedSign/index.js
+++ b/app/components/UI/TypedSign/index.js
@@ -292,6 +292,7 @@ class TypedSign extends PureComponent {
truncateMessage={truncateMessage}
type="typedSign"
fromAddress={from}
+ testID={'typed-signature-request'}
>
StyleSheet.create({
@@ -102,15 +101,6 @@ const WatchAssetRequest = ({
? strings('transaction.failed')
: `${renderFromTokenMinimalUnit(balance, asset.decimals)} ${asset.symbol}`;
- useEffect(
- () => async () => {
- const { TokensController } = Engine.context;
- typeof suggestedAssetMeta !== undefined &&
- (await TokensController.rejectWatchAsset(suggestedAssetMeta.id));
- },
- [suggestedAssetMeta],
- );
-
const getAnalyticsParams = () => {
try {
const { NetworkController } = Engine.context;
@@ -131,13 +121,7 @@ const WatchAssetRequest = ({
};
const onConfirmPress = async () => {
- const { TokensController } = Engine.context;
- await TokensController.acceptWatchAsset(
- suggestedAssetMeta.id,
- // TODO - Ideally, this is already checksummed.
- safeToChecksumAddress(interactingAddress),
- );
- onConfirm && onConfirm();
+ await onConfirm();
InteractionManager.runAfterInteractions(() => {
AnalyticsV2.trackEvent(
MetaMetricsEvents.TOKEN_ADDED,
diff --git a/app/components/Views/AccountAction/AccountAction.styles.ts b/app/components/Views/AccountAction/AccountAction.styles.ts
index b86cd5253a1..bac7b828464 100644
--- a/app/components/Views/AccountAction/AccountAction.styles.ts
+++ b/app/components/Views/AccountAction/AccountAction.styles.ts
@@ -20,7 +20,7 @@ const styleSheet = (params: {
vars: TouchableOpacityStyleSheetVars;
}) => {
const { theme, vars } = params;
- const { style } = vars;
+ const { style, disabled } = vars;
const { colors } = theme;
return StyleSheet.create({
@@ -34,10 +34,11 @@ const styleSheet = (params: {
style,
) as ViewStyle,
descriptionLabel: {
- color: colors.text.alternative,
+ color: disabled ? colors.text.muted : colors.text.default,
},
icon: {
marginHorizontal: 16,
+ color: disabled ? colors.text.muted : colors.text.default,
},
});
};
diff --git a/app/components/Views/AccountAction/AccountAction.tsx b/app/components/Views/AccountAction/AccountAction.tsx
index 86125de7638..14bec320192 100644
--- a/app/components/Views/AccountAction/AccountAction.tsx
+++ b/app/components/Views/AccountAction/AccountAction.tsx
@@ -18,14 +18,17 @@ const AccountAction = ({
iconName,
iconSize = IconSize.Md,
style,
+ disabled = false,
...props
}: WalletActionProps) => {
- const { styles } = useStyles(styleSheet, { style });
+ const { styles } = useStyles(styleSheet, { style, disabled });
return (
-
+
- {actionTitle}
+
+ {actionTitle}
+
);
};
diff --git a/app/components/Views/AccountAction/AccountAction.types.ts b/app/components/Views/AccountAction/AccountAction.types.ts
index 2878d5e92de..dc464c40fa2 100644
--- a/app/components/Views/AccountAction/AccountAction.types.ts
+++ b/app/components/Views/AccountAction/AccountAction.types.ts
@@ -8,6 +8,7 @@ export interface WalletActionProps extends TouchableOpacityProps {
actionTitle: string;
iconName: IconName;
iconSize?: IconSize;
+ disabled?: boolean;
}
/**
@@ -15,5 +16,5 @@ export interface WalletActionProps extends TouchableOpacityProps {
*/
export type TouchableOpacityStyleSheetVars = Pick<
TouchableOpacityProps,
- 'style'
+ 'style' | 'disabled'
>;
diff --git a/app/components/Views/AccountActions/AccountActions.test.tsx b/app/components/Views/AccountActions/AccountActions.test.tsx
index d6203fda9c5..cf57cd37470 100644
--- a/app/components/Views/AccountActions/AccountActions.test.tsx
+++ b/app/components/Views/AccountActions/AccountActions.test.tsx
@@ -54,9 +54,18 @@ jest.mock('@react-navigation/native', () => {
};
});
-jest.mock('react-native-safe-area-context', () => ({
- useSafeAreaInsets: () => ({ top: 0, left: 0, right: 0, bottom: 0 }),
-}));
+jest.mock('react-native-safe-area-context', () => {
+ const inset = { top: 0, right: 0, bottom: 0, left: 0 };
+ const frame = { width: 0, height: 0, x: 0, y: 0 };
+ return {
+ SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children),
+ SafeAreaConsumer: jest
+ .fn()
+ .mockImplementation(({ children }) => children(inset)),
+ useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
+ useSafeAreaFrame: jest.fn().mockImplementation(() => frame),
+ };
+});
jest.mock('react-native-share', () => ({
open: jest.fn(() => Promise.resolve()),
diff --git a/app/components/Views/AccountActions/AccountActions.tsx b/app/components/Views/AccountActions/AccountActions.tsx
index 7b939b11ea4..3d0706121f4 100644
--- a/app/components/Views/AccountActions/AccountActions.tsx
+++ b/app/components/Views/AccountActions/AccountActions.tsx
@@ -144,7 +144,6 @@ const AccountActions = () => {
diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx
index 26e051b2609..a027349b2bf 100644
--- a/app/components/Views/AccountConnect/AccountConnect.tsx
+++ b/app/components/Views/AccountConnect/AccountConnect.tsx
@@ -372,6 +372,7 @@ const AccountConnect = (props: AccountConnectProps) => {
secureIcon={secureIcon}
urlWithProtocol={urlWithProtocol}
onUserAction={setUserIntent}
+ onBack={() => setScreen(AccountConnectScreens.SingleConnect)}
/>
),
[
diff --git a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.styles.ts b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.styles.ts
index 3b20bc32622..0e85d288de6 100644
--- a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.styles.ts
+++ b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.styles.ts
@@ -35,6 +35,10 @@ const styleSheet = (params: { theme: Theme }) => {
disabled: {
opacity: 0.5,
},
+ addAccountButtonContainer: {
+ marginHorizontal: 16,
+ marginTop: 16,
+ },
});
};
diff --git a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx
index d389acad3bb..e9cb31f0070 100644
--- a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx
+++ b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx
@@ -1,9 +1,8 @@
// Third party dependencies.
-import React, { useCallback } from 'react';
+import React, { useCallback, useState } from 'react';
import { View, Platform } from 'react-native';
// External dependencies.
-import SheetActions from '../../../../component-library/components-temp/SheetActions';
import SheetHeader from '../../../../component-library/components/Sheet/SheetHeader';
import { strings } from '../../../../../locales/i18n';
import TagUrl from '../../../../component-library/components/Tags/TagUrl';
@@ -12,16 +11,22 @@ import { useStyles } from '../../../../component-library/hooks';
import Button, {
ButtonSize,
ButtonVariants,
+ ButtonWidthTypes,
} from '../../../../component-library/components/Buttons/Button';
import AccountSelectorList from '../../../UI/AccountSelectorList';
-
-// Internal dependencies.
-import styleSheet from './AccountConnectMultiSelector.styles';
-import { AccountConnectMultiSelectorProps } from './AccountConnectMultiSelector.types';
import USER_INTENT from '../../../../constants/permissions';
import generateTestId from '../../../../../wdio/utils/generateTestId';
import { ACCOUNT_APPROVAL_SELECT_ALL_BUTTON } from '../../../../../wdio/screen-objects/testIDs/Components/AccountApprovalModal.testIds';
+// Internal dependencies.
+import styleSheet from './AccountConnectMultiSelector.styles';
+import {
+ AccountConnectMultiSelectorProps,
+ AccountConnectMultiSelectorScreens,
+} from './AccountConnectMultiSelector.types';
+import AddAccountActions from '../../AddAccountActions';
+import { ACCOUNT_LIST_ADD_BUTTON_ID } from '../../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds';
+
const AccountConnectMultiSelector = ({
accounts,
ensByAccountAddress,
@@ -33,8 +38,12 @@ const AccountConnectMultiSelector = ({
secureIcon,
isAutoScrollEnabled = true,
urlWithProtocol,
+ onBack,
}: AccountConnectMultiSelectorProps) => {
const { styles } = useStyles(styleSheet, {});
+ const [screen, setScreen] = useState(
+ AccountConnectMultiSelectorScreens.AccountMultiSelector,
+ );
const onSelectAccount = useCallback(
(accAddress) => {
@@ -53,31 +62,6 @@ const AccountConnectMultiSelector = ({
[accounts, selectedAddresses, onSelectAddress],
);
- const renderSheetActions = useCallback(
- () => (
- onUserAction(USER_INTENT.CreateMultiple),
- isLoading,
- },
- {
- label: strings('accounts.import_account'),
- onPress: () => onUserAction(USER_INTENT.Import),
- disabled: isLoading,
- },
- {
- label: strings('accounts.connect_hardware'),
- onPress: () => onUserAction(USER_INTENT.ConnectHW),
- disabled: isLoading,
- },
- ]}
- />
- ),
- [isLoading, onUserAction],
- );
-
const renderSelectAllButton = useCallback(
() =>
Boolean(accounts.length) && (
@@ -157,36 +141,95 @@ const AccountConnectMultiSelector = ({
.map(({ address }) => address)
.every((address) => selectedAddresses.includes(address));
- return (
- <>
-
-
- (
+ <>
+
-
- {strings('accounts.connect_description')}
-
- {areAllAccountsSelected
- ? renderUnselectAllButton()
- : renderSelectAllButton()}
-
-
+
+
+ {strings('accounts.connect_description')}
+
+ {areAllAccountsSelected
+ ? renderUnselectAllButton()
+ : renderSelectAllButton()}
+
+
+
+
+ {renderCtaButtons()}
+ >
+ ),
+ [
+ accounts,
+ areAllAccountsSelected,
+ ensByAccountAddress,
+ favicon,
+ isAutoScrollEnabled,
+ isLoading,
+ onSelectAccount,
+ renderCtaButtons,
+ renderSelectAllButton,
+ renderUnselectAllButton,
+ secureIcon,
+ selectedAddresses,
+ styles.addAccountButtonContainer,
+ styles.body,
+ styles.description,
+ urlWithProtocol,
+ onBack,
+ ],
+ );
+
+ const renderAddAccountActions = useCallback(
+ () => (
+
+ setScreen(AccountConnectMultiSelectorScreens.AccountMultiSelector)
+ }
/>
- {renderSheetActions()}
- {renderCtaButtons()}
- >
+ ),
+ [],
);
+
+ const renderAccountScreens = useCallback(() => {
+ switch (screen) {
+ case AccountConnectMultiSelectorScreens.AccountMultiSelector:
+ return renderAccountConnectMultiSelector();
+ case AccountConnectMultiSelectorScreens.AddAccountActions:
+ return renderAddAccountActions();
+ default:
+ return renderAccountConnectMultiSelector();
+ }
+ }, [screen, renderAccountConnectMultiSelector, renderAddAccountActions]);
+
+ return renderAccountScreens();
};
export default AccountConnectMultiSelector;
diff --git a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts
index 1ba9cdaf004..7a4420f58c8 100644
--- a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts
+++ b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts
@@ -1,6 +1,14 @@
// Third party dependencies.
import { ImageSourcePropType } from 'react-native';
+/**
+ * Enum to track states of the account connect multi selector screen.
+ */
+export enum AccountConnectMultiSelectorScreens {
+ AccountMultiSelector = 'AccountMultiSelector',
+ AddAccountActions = 'AddAccountActions',
+}
+
// External dependencies.
import { UseAccounts } from '../../../hooks/useAccounts';
import { IconName } from '../../../../component-library/components/Icons/Icon';
@@ -18,4 +26,5 @@ export interface AccountConnectMultiSelectorProps extends UseAccounts {
favicon: ImageSourcePropType;
secureIcon: IconName;
isAutoScrollEnabled?: boolean;
+ onBack: () => void;
}
diff --git a/app/components/Views/AccountPermissions/AccountPermissions.tsx b/app/components/Views/AccountPermissions/AccountPermissions.tsx
old mode 100644
new mode 100755
index 07f296cd0d2..b220895e4b3
--- a/app/components/Views/AccountPermissions/AccountPermissions.tsx
+++ b/app/components/Views/AccountPermissions/AccountPermissions.tsx
@@ -362,6 +362,7 @@ const AccountPermissions = (props: AccountPermissionsProps) => {
urlWithProtocol={urlWithProtocol}
secureIcon={secureIcon}
isAutoScrollEnabled={false}
+ onBack={() => setPermissionsScreen(AccountPermissionsScreens.Connected)}
/>
),
[
diff --git a/app/components/Views/AccountPermissions/AccountPermissionsConnected/AccountPermissionsConnected.tsx b/app/components/Views/AccountPermissions/AccountPermissionsConnected/AccountPermissionsConnected.tsx
index 9286129255c..6892737239c 100644
--- a/app/components/Views/AccountPermissions/AccountPermissionsConnected/AccountPermissionsConnected.tsx
+++ b/app/components/Views/AccountPermissions/AccountPermissionsConnected/AccountPermissionsConnected.tsx
@@ -113,9 +113,6 @@ const AccountPermissionsConnected = ({
const switchNetwork = useCallback(() => {
navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.NETWORK_SELECTOR,
- params: {
- fromAccountPermission: true,
- },
});
AnalyticsV2.trackEvent(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED, {
diff --git a/app/components/Views/AccountSelector/AccountSelector.constants.ts b/app/components/Views/AccountSelector/AccountSelector.constants.ts
index b41f8b4becb..3052354472a 100644
--- a/app/components/Views/AccountSelector/AccountSelector.constants.ts
+++ b/app/components/Views/AccountSelector/AccountSelector.constants.ts
@@ -1,5 +1,3 @@
/* eslint-disable import/prefer-default-export */
export const ACCOUNT_LIST_ID = 'account-list';
-export const CREATE_ACCOUNT_BUTTON_ID = 'create-account-button';
-export const IMPORT_ACCOUNT_BUTTON_ID = 'import-account-button';
diff --git a/app/components/Views/AccountSelector/AccountSelector.styles.ts b/app/components/Views/AccountSelector/AccountSelector.styles.ts
index b6113ad83aa..3d12bee70c5 100644
--- a/app/components/Views/AccountSelector/AccountSelector.styles.ts
+++ b/app/components/Views/AccountSelector/AccountSelector.styles.ts
@@ -2,6 +2,7 @@ import { StyleSheet } from 'react-native';
export default StyleSheet.create({
sheet: {
- paddingBottom: 16,
+ marginVertical: 16,
+ marginHorizontal: 16,
},
});
diff --git a/app/components/Views/AccountSelector/AccountSelector.tsx b/app/components/Views/AccountSelector/AccountSelector.tsx
index ccb559a53eb..1e47d249e48 100644
--- a/app/components/Views/AccountSelector/AccountSelector.tsx
+++ b/app/components/Views/AccountSelector/AccountSelector.tsx
@@ -1,62 +1,64 @@
// Third party dependencies.
-import React, { useCallback, useRef, useState } from 'react';
+import React, { Fragment, useCallback, useRef, useState } from 'react';
import { InteractionManager, Platform, View } from 'react-native';
-import { useNavigation } from '@react-navigation/native';
// External dependencies.
import AccountSelectorList from '../../UI/AccountSelectorList';
-import SheetActions from '../../../component-library/components-temp/SheetActions';
import SheetBottom, {
SheetBottomRef,
} from '../../../component-library/components/Sheet/SheetBottom';
import SheetHeader from '../../../component-library/components/Sheet/SheetHeader';
import UntypedEngine from '../../../core/Engine';
-import Logger from '../../../util/Logger';
import AnalyticsV2 from '../../../util/analyticsV2';
import { MetaMetricsEvents } from '../../../core/Analytics';
import { strings } from '../../../../locales/i18n';
import { useAccounts } from '../../hooks/useAccounts';
import generateTestId from '../../../../wdio/utils/generateTestId';
-// Internal dependencies.
+import Button, {
+ ButtonSize,
+ ButtonVariants,
+ ButtonWidthTypes,
+} from '../../../component-library/components/Buttons/Button';
+import AddAccountActions from '../AddAccountActions';
import {
ACCOUNT_LIST_ID,
- CREATE_ACCOUNT_BUTTON_ID,
- IMPORT_ACCOUNT_BUTTON_ID,
-} from './AccountSelector.constants';
-import { AccountSelectorProps } from './AccountSelector.types';
+ ACCOUNT_LIST_ADD_BUTTON_ID,
+} from '../../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds';
+
+// Internal dependencies.
+import {
+ AccountSelectorProps,
+ AccountSelectorScreens,
+} from './AccountSelector.types';
import styles from './AccountSelector.styles';
const AccountSelector = ({ route }: AccountSelectorProps) => {
- const {
- onCreateNewAccount,
- onOpenImportAccount,
- onOpenConnectHardwareWallet,
- onSelectAccount,
- checkBalanceError,
- isSelectOnly,
- } = route.params || {};
+ const { onSelectAccount, checkBalanceError } = route.params || {};
const Engine = UntypedEngine as any;
- const [isLoading, setIsLoading] = useState(false);
const sheetRef = useRef(null);
- const navigation = useNavigation();
const { accounts, ensByAccountAddress } = useAccounts({
checkBalanceError,
- isLoading,
});
+ const [screen, setScreen] = useState(
+ AccountSelectorScreens.AccountSelector,
+ );
- const _onSelectAccount = (address: string) => {
- const { PreferencesController } = Engine.context;
- PreferencesController.setSelectedAddress(address);
- sheetRef.current?.hide();
- onSelectAccount?.(address);
- InteractionManager.runAfterInteractions(() => {
- // Track Event: "Switched Account"
- AnalyticsV2.trackEvent(MetaMetricsEvents.SWITCHED_ACCOUNT, {
- source: 'Wallet Tab',
- number_of_accounts: accounts?.length,
+ const _onSelectAccount = useCallback(
+ (address: string) => {
+ const { PreferencesController } = Engine.context;
+ PreferencesController.setSelectedAddress(address);
+ sheetRef.current?.hide();
+ onSelectAccount?.(address);
+ InteractionManager.runAfterInteractions(() => {
+ // Track Event: "Switched Account"
+ AnalyticsV2.trackEvent(MetaMetricsEvents.SWITCHED_ACCOUNT, {
+ source: 'Wallet Tab',
+ number_of_accounts: accounts?.length,
+ });
});
- });
- };
+ },
+ [Engine.context, accounts?.length, onSelectAccount],
+ );
const onRemoveImportedAccount = useCallback(
({ nextActiveAddress }: { nextActiveAddress: string }) => {
@@ -67,85 +69,54 @@ const AccountSelector = ({ route }: AccountSelectorProps) => {
[Engine.context],
);
- const createNewAccount = useCallback(async () => {
- const { KeyringController, PreferencesController } = Engine.context;
- try {
- setIsLoading(true);
- const { addedAccountAddress } = await KeyringController.addNewAccount();
- PreferencesController.setSelectedAddress(addedAccountAddress);
- AnalyticsV2.trackEvent(MetaMetricsEvents.ACCOUNTS_ADDED_NEW_ACCOUNT, {});
- } catch (e: any) {
- Logger.error(e, 'error while trying to add a new account');
- } finally {
- setIsLoading(false);
- }
- onCreateNewAccount?.();
- /* eslint-disable-next-line */
- }, [onCreateNewAccount, setIsLoading]);
-
- const openImportAccount = useCallback(() => {
- navigation.navigate('ImportPrivateKeyView');
- // Is this where we want to track importing an account or within ImportPrivateKeyView screen?
- AnalyticsV2.trackEvent(MetaMetricsEvents.ACCOUNTS_IMPORTED_NEW_ACCOUNT, {});
- onOpenImportAccount?.();
- }, [onOpenImportAccount, navigation]);
-
- const openConnectHardwareWallet = useCallback(() => {
- navigation.navigate('ConnectQRHardwareFlow');
- // Is this where we want to track connecting a hardware wallet or within ConnectQRHardwareFlow screen?
- AnalyticsV2.trackEvent(MetaMetricsEvents.CONNECT_HARDWARE_WALLET, {});
- onOpenConnectHardwareWallet?.();
- }, [onOpenConnectHardwareWallet, navigation]);
-
- const renderSheetActions = useCallback(
- () =>
- !isSelectOnly && (
- (
+
+
+
- ),
- [
- isSelectOnly,
- isLoading,
- createNewAccount,
- openImportAccount,
- openConnectHardwareWallet,
- ],
+
+
+
+ ),
+ [accounts, _onSelectAccount, ensByAccountAddress, onRemoveImportedAccount],
);
- return (
-
-
- (
+ setScreen(AccountSelectorScreens.AccountSelector)}
/>
- {renderSheetActions()}
-
+ ),
+ [],
);
+
+ const renderAccountScreens = useCallback(() => {
+ switch (screen) {
+ case AccountSelectorScreens.AccountSelector:
+ return renderAccountSelector();
+ case AccountSelectorScreens.AddAccountActions:
+ return renderAddAccountActions();
+ default:
+ return renderAccountSelector();
+ }
+ }, [screen, renderAccountSelector, renderAddAccountActions]);
+
+ return {renderAccountScreens()};
};
export default AccountSelector;
diff --git a/app/components/Views/AccountSelector/AccountSelector.types.ts b/app/components/Views/AccountSelector/AccountSelector.types.ts
index b8f5e2e18c2..99f79c3bc40 100644
--- a/app/components/Views/AccountSelector/AccountSelector.types.ts
+++ b/app/components/Views/AccountSelector/AccountSelector.types.ts
@@ -1,6 +1,14 @@
// External dependencies.
import { UseAccountsParams } from '../../../components/hooks/useAccounts';
+/**
+ * Enum to track states of the account selector screen.
+ */
+export enum AccountSelectorScreens {
+ AccountSelector = 'AccountSelector',
+ AddAccountActions = 'AddAccountActions',
+}
+
export interface AccountSelectorParams {
/**
* Optional callback that is called whenever a new account is being created.
diff --git a/app/components/Views/AddAccountActions/AddAccountActions.tsx b/app/components/Views/AddAccountActions/AddAccountActions.tsx
new file mode 100644
index 00000000000..e6cfcbff22a
--- /dev/null
+++ b/app/components/Views/AddAccountActions/AddAccountActions.tsx
@@ -0,0 +1,89 @@
+// Third party dependencies.
+import React, { Fragment, useCallback, useState } from 'react';
+import { Platform, View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+// External dependencies.
+import SheetHeader from '../../../component-library/components/Sheet/SheetHeader';
+import AccountAction from '../AccountAction/AccountAction';
+import { IconName } from '../../../component-library/components/Icons/Icon';
+import { strings } from '../../../../locales/i18n';
+import AnalyticsV2 from '../../../util/analyticsV2';
+import { MetaMetricsEvents } from '../../../core/Analytics';
+import Logger from '../../../util/Logger';
+import Engine from '../../../core/Engine';
+
+// Internal dependencies
+import { AddAccountActionsProps } from './AddAccountActions.types';
+import generateTestId from '../../../../wdio/utils/generateTestId';
+import {
+ ADD_ACCOUNT_NEW_ACCOUNT_BUTTON,
+ ADD_ACCOUNT_IMPORT_ACCOUNT_BUTTON,
+} from '../../../../wdio/screen-objects/testIDs/Components/AddAccountModal.testIds';
+
+const AddAccountActions = ({ onBack }: AddAccountActionsProps) => {
+ const { navigate } = useNavigation();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const openImportAccount = useCallback(() => {
+ navigate('ImportPrivateKeyView');
+ onBack();
+ AnalyticsV2.trackEvent(MetaMetricsEvents.ACCOUNTS_IMPORTED_NEW_ACCOUNT, {});
+ }, [navigate, onBack]);
+
+ const openConnectHardwareWallet = useCallback(() => {
+ navigate('ConnectQRHardwareFlow');
+ onBack();
+ AnalyticsV2.trackEvent(MetaMetricsEvents.CONNECT_HARDWARE_WALLET, {});
+ }, [onBack, navigate]);
+
+ const createNewAccount = useCallback(async () => {
+ const { KeyringController, PreferencesController } = Engine.context;
+ try {
+ setIsLoading(true);
+
+ const { addedAccountAddress } = await KeyringController.addNewAccount();
+ PreferencesController.setSelectedAddress(addedAccountAddress);
+ AnalyticsV2.trackEvent(MetaMetricsEvents.ACCOUNTS_ADDED_NEW_ACCOUNT, {});
+ } catch (e: any) {
+ Logger.error(e, 'error while trying to add a new account');
+ } finally {
+ onBack();
+
+ setIsLoading(false);
+ }
+ }, [onBack, setIsLoading]);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AddAccountActions;
diff --git a/app/components/Views/AddAccountActions/AddAccountActions.types.ts b/app/components/Views/AddAccountActions/AddAccountActions.types.ts
new file mode 100644
index 00000000000..e5b0e903395
--- /dev/null
+++ b/app/components/Views/AddAccountActions/AddAccountActions.types.ts
@@ -0,0 +1,3 @@
+export interface AddAccountActionsProps {
+ onBack: () => void;
+}
diff --git a/app/components/Views/AddAccountActions/index.tsx b/app/components/Views/AddAccountActions/index.tsx
new file mode 100644
index 00000000000..2b465109936
--- /dev/null
+++ b/app/components/Views/AddAccountActions/index.tsx
@@ -0,0 +1 @@
+export { default } from './AddAccountActions';
diff --git a/app/components/Views/Approval/index.js b/app/components/Views/Approval/index.js
index a39748e317b..a81c14becb8 100644
--- a/app/components/Views/Approval/index.js
+++ b/app/components/Views/Approval/index.js
@@ -82,9 +82,9 @@ class Approval extends PureComponent {
*/
networkType: PropTypes.string,
/**
- * Hides or shows the dApp transaction modal
+ * Hide dapp transaction modal
*/
- toggleDappTransactionModal: PropTypes.func,
+ hideModal: PropTypes.func,
/**
* Tells whether or not dApp transaction modal is visible
*/
@@ -183,7 +183,7 @@ class Approval extends PureComponent {
Engine.context.TransactionController.cancelTransaction(
transaction.id,
);
- this.props.toggleDappTransactionModal(false);
+ this.props.hideModal();
}
} catch (e) {
if (e) {
@@ -308,7 +308,7 @@ class Approval extends PureComponent {
};
onCancel = () => {
- this.props.toggleDappTransactionModal();
+ this.props.hideModal();
this.state.mode === REVIEW && this.trackOnCancel();
this.showWalletConnectNotification();
AnalyticsV2.trackEvent(
@@ -354,7 +354,7 @@ class Approval extends PureComponent {
(transactionMeta) => {
if (transactionMeta.status === 'submitted') {
this.setState({ transactionHandled: true });
- this.props.toggleDappTransactionModal();
+ this.props.hideModal();
NotificationManager.watchSubmittedTransaction({
...transactionMeta,
assetType: transaction.assetType,
diff --git a/app/components/Views/ApproveView/Approve/index.js b/app/components/Views/ApproveView/Approve/index.js
index a6dbb48d31f..232243bc68b 100644
--- a/app/components/Views/ApproveView/Approve/index.js
+++ b/app/components/Views/ApproveView/Approve/index.js
@@ -93,9 +93,9 @@ class Approve extends PureComponent {
*/
modalVisible: PropTypes.bool,
/**
- /* Token approve modal visible or not
+ /* Hide modal visible or not
*/
- toggleApproveModal: PropTypes.func,
+ hideModal: PropTypes.func,
/**
* Current selected ticker
*/
@@ -159,6 +159,8 @@ class Approve extends PureComponent {
legacyGasTransaction: {},
isBlockExplorerVisible: false,
address: '',
+ tokenAllowanceState: undefined,
+ isGasEstimateStatusIn: false,
};
computeGasEstimates = (overrideGasLimit, gasEstimateTypeChanged) => {
@@ -239,7 +241,7 @@ class Approve extends PureComponent {
componentDidMount = async () => {
const { showCustomNonce } = this.props;
if (!this.props?.transaction?.id) {
- this.props.toggleApproveModal(false);
+ this.props.hideModal();
return null;
}
if (!this.props?.transaction?.gas) this.handleGetGasLimit();
@@ -297,7 +299,7 @@ class Approve extends PureComponent {
transaction &&
transaction.id &&
Engine.context.TransactionController.cancelTransaction(transaction.id);
- this.props.toggleApproveModal(false);
+ this.props.hideModal();
}
};
@@ -447,7 +449,7 @@ class Approve extends PureComponent {
(transactionMeta) => {
if (transactionMeta.status === 'submitted') {
this.setState({ approved: true });
- this.props.toggleApproveModal();
+ this.props.hideModal();
NotificationManager.watchSubmittedTransaction({
...transactionMeta,
assetType: 'ETH',
@@ -492,7 +494,7 @@ class Approve extends PureComponent {
MetaMetricsEvents.APPROVAL_CANCELLED,
this.getAnalyticsParams(),
);
- this.props.toggleApproveModal(false);
+ this.props.hideModal();
NotificationManager.showSimpleNotification({
status: `simple_notification_rejected`,
@@ -567,6 +569,7 @@ class Approve extends PureComponent {
this.setState({
eip1559GasTransaction: gas,
legacyGasTransaction: gas,
+ isGasEstimateStatusIn: true,
gasError,
});
};
@@ -577,6 +580,10 @@ class Approve extends PureComponent {
});
};
+ updateTokenAllowanceState = (value) => {
+ this.setState({ tokenAllowanceState: value });
+ };
+
render = () => {
const colors = this.context.colors || mockTheme.colors;
const styles = createStyles(colors);
@@ -595,6 +602,8 @@ class Approve extends PureComponent {
gasError,
address,
shouldAddNickname,
+ tokenAllowanceState,
+ isGasEstimateStatusIn,
} = this.state;
const {
@@ -712,7 +721,6 @@ class Approve extends PureComponent {
savedContactListToArray={savedContactListToArray}
transactionConfirmed={transactionConfirmed}
showBlockExplorer={this.setIsBlockExplorerVisible}
- onUpdateContractNickname={this.onUpdateContractNickname}
toggleModal={this.toggleModal}
showVerifyContractDetails={this.showVerifyContractDetails}
shouldVerifyContractDetails={
@@ -726,9 +734,12 @@ class Approve extends PureComponent {
: ''
}
chainId={chainId}
+ updateTokenAllowanceState={this.updateTokenAllowanceState}
+ tokenAllowanceState={tokenAllowanceState}
updateTransactionState={this.updateTransactionState}
legacyGasObject={this.state.legacyGasObject}
eip1559GasObject={this.state.eip1559GasObject}
+ isGasEstimateStatusIn={isGasEstimateStatusIn}
/>
{/** View fixes layout issue after removing */}
diff --git a/app/components/Views/ConnectQRHardware/Instruction/index.tsx b/app/components/Views/ConnectQRHardware/Instruction/index.tsx
index e3437076f17..0845eca3587 100644
--- a/app/components/Views/ConnectQRHardware/Instruction/index.tsx
+++ b/app/components/Views/ConnectQRHardware/Instruction/index.tsx
@@ -5,6 +5,7 @@ import React from 'react';
import { View, Text, StyleSheet, Image, ScrollView } from 'react-native';
import { strings } from '../../../../../locales/i18n';
import {
+ KEYSTONE_LEARN_MORE,
KEYSTONE_SUPPORT,
KEYSTONE_SUPPORT_VIDEO,
} from '../../../../constants/urls';
@@ -79,6 +80,17 @@ const createStyles = (colors: any) =>
marginTop: 40,
marginBottom: 40,
},
+ keystone: {
+ height: 48,
+ fontSize: 24,
+ },
+ buttonGroup: {
+ display: 'flex',
+ flexDirection: 'row',
+ },
+ linkMarginRight: {
+ marginRight: 16,
+ },
});
const ConnectQRInstruction = (props: IConnectQRInstructionProps) => {
@@ -95,6 +107,15 @@ const ConnectQRInstruction = (props: IConnectQRInstructionProps) => {
},
});
};
+ const navigateToLearnMore = () => {
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url: KEYSTONE_LEARN_MORE,
+ title: strings('connect_qr_hardware.keystone'),
+ },
+ });
+ };
const navigateToTutorial = () => {
navigation.navigate('Webview', {
screen: 'SimpleWebview',
@@ -122,9 +143,23 @@ const ConnectQRInstruction = (props: IConnectQRInstructionProps) => {
{strings('connect_qr_hardware.description3')}
-
- {strings('connect_qr_hardware.description4')}
+
+ {strings('connect_qr_hardware.keystone')}
+
+
+ {strings('connect_qr_hardware.learnMore')}
+
+
+ {strings('connect_qr_hardware.tutorial')}
+
+
{strings('connect_qr_hardware.description5')}
diff --git a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx
index 1035f348c00..c5ef44e5139 100644
--- a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx
+++ b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx
@@ -97,15 +97,6 @@ const initialState = {
'https://optimism-mainnet.infura.io/v3/cda392a134014865ad3c273dc7ddfff3',
ticker: 'ETH',
},
- {
- chainId: '59140',
- nickname: 'Linea Goerli Test Network',
- rpcPrefs: {
- blockExplorerUrl: 'https://explorer.goerli.linea.build',
- },
- rpcUrl: 'https://rpc.goerli.linea.build/',
- ticker: 'LineaETH',
- },
{
chainId: '100',
nickname: 'Gnosis Chain',
diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx
index 9f8dabce20c..9955c6da98a 100644
--- a/app/components/Views/NetworkSelector/NetworkSelector.tsx
+++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx
@@ -1,8 +1,8 @@
// Third party dependencies.
-import React, { useRef } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
+import { Platform } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import images from 'images/image-icons';
-import urlParse from 'url-parse';
import { useNavigation } from '@react-navigation/native';
import { FrequentRpc } from '@metamask/preferences-controller';
import { ProviderConfig } from '@metamask/network-controller';
@@ -22,20 +22,11 @@ import { selectProviderConfig } from '../../../selectors/networkController';
import Networks, {
compareRpcUrls,
getAllNetworks,
- getDecimalChainId,
getNetworkImageSource,
+ shouldShowLineaMainnetNetwork,
} from '../../../util/networks';
import { EngineState } from 'app/selectors/types';
-import {
- LINEA_TESTNET_NICKNAME,
- LINEA_TESTNET_TICKER,
- MAINNET,
- NETWORKS_CHAIN_ID,
-} from '../../../constants/network';
-import {
- LINEA_TESTNET_BLOCK_EXPLORER,
- LINEA_TESTNET_RPC_URL,
-} from '../../../constants/urls';
+import { LINEA_MAINNET, MAINNET } from '../../../constants/network';
import Button from '../../../component-library/components/Buttons/Button/Button';
import {
ButtonSize,
@@ -45,13 +36,13 @@ import {
import Engine from '../../../core/Engine';
import analyticsV2 from '../../../util/analyticsV2';
import { MetaMetricsEvents } from '../../../core/Analytics';
+import Routes from '../../../constants/navigation/Routes';
+import generateTestId from '../../../../wdio/utils/generateTestId';
+import { ADD_NETWORK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/NetworksScreen.testids';
import { NETWORK_SCROLL_ID } from '../../../../wdio/screen-objects/testIDs/Components/NetworkListModal.TestIds';
// Internal dependencies
import styles from './NetworkSelector.styles';
-import generateTestId from '../../../../wdio/utils/generateTestId';
-import { Platform } from 'react-native';
-import { ADD_NETWORK_BUTTON } from '../../../../wdio/screen-objects/testIDs/Screens/NetworksScreen.testids';
const NetworkSelector = () => {
const { navigate } = useNavigation();
@@ -61,6 +52,7 @@ const NetworkSelector = () => {
const thirdPartyApiMode = useSelector(
(state: any) => state.privacy.thirdPartyApiMode,
);
+ const [lineaMainnetReleased, setLineaMainnetReleased] = useState(false);
const providerConfig: ProviderConfig = useSelector(selectProviderConfig);
const frequentRpcList: FrequentRpc[] = useSelector(
@@ -68,6 +60,14 @@ const NetworkSelector = () => {
state.engine.backgroundState.PreferencesController.frequentRpcList,
);
+ useEffect(() => {
+ const shouldShowLineaMainnet = shouldShowLineaMainnetNetwork();
+
+ if (shouldShowLineaMainnet) {
+ setLineaMainnetReleased(shouldShowLineaMainnet);
+ }
+ }, []);
+
const onNetworkChange = (type: string) => {
const { NetworkController, CurrencyRateController } = Engine.context;
CurrencyRateController.setNativeCurrency('ETH');
@@ -90,67 +90,26 @@ const NetworkSelector = () => {
};
const onSetRpcTarget = async (rpcTarget: string) => {
- const { PreferencesController, CurrencyRateController, NetworkController } =
- Engine.context;
-
- const isLineaTestnetInFrequentRpcList =
- frequentRpcList.findIndex(
- (frequentRpc: FrequentRpc) =>
- frequentRpc.chainId?.toString() === NETWORKS_CHAIN_ID.LINEA_TESTNET,
- ) !== -1;
+ const { CurrencyRateController, NetworkController } = Engine.context;
- let rpc = frequentRpcList.find(({ rpcUrl }: { rpcUrl: string }) =>
+ const rpc = frequentRpcList.find(({ rpcUrl }: { rpcUrl: string }) =>
compareRpcUrls(rpcUrl, rpcTarget),
);
- if (
- !isLineaTestnetInFrequentRpcList &&
- compareRpcUrls(rpcTarget, LINEA_TESTNET_RPC_URL)
- ) {
- const url = new urlParse(LINEA_TESTNET_RPC_URL);
- const decimalChainId = getDecimalChainId(NETWORKS_CHAIN_ID.LINEA_TESTNET);
+ if (rpc) {
+ const { rpcUrl, chainId, ticker, nickname } = rpc;
- PreferencesController.addToFrequentRpcList(
- url.href,
- decimalChainId,
- LINEA_TESTNET_TICKER,
- LINEA_TESTNET_NICKNAME,
- {
- blockExplorerUrl: LINEA_TESTNET_BLOCK_EXPLORER,
- },
- );
-
- const analyticsParamsAdd = {
- chain_id: decimalChainId,
- source: 'Popular network list',
- symbol: LINEA_TESTNET_TICKER,
- };
+ CurrencyRateController.setNativeCurrency(ticker);
- analyticsV2.trackEvent(
- MetaMetricsEvents.NETWORK_ADDED,
- analyticsParamsAdd,
- );
+ NetworkController.setRpcTarget(rpcUrl, chainId, ticker, nickname);
- rpc = {
- rpcUrl: url.href,
- chainId: decimalChainId,
- ticker: LINEA_TESTNET_TICKER,
- nickname: LINEA_TESTNET_NICKNAME,
- };
+ sheetRef.current?.hide();
+ analyticsV2.trackEvent(MetaMetricsEvents.NETWORK_SWITCHED, {
+ chain_id: providerConfig.chainId,
+ from_network: providerConfig.type,
+ to_network: nickname,
+ });
}
-
- const { rpcUrl, chainId, ticker, nickname } = rpc;
-
- CurrencyRateController.setNativeCurrency(ticker);
-
- NetworkController.setRpcTarget(rpcUrl, chainId, ticker, nickname);
-
- sheetRef.current?.hide();
- analyticsV2.trackEvent(MetaMetricsEvents.NETWORK_SWITCHED, {
- chain_id: providerConfig.chainId,
- from_network: providerConfig.type,
- to_network: nickname,
- });
};
const renderMainnet = () => {
@@ -164,27 +123,42 @@ const NetworkSelector = () => {
name: mainnetName,
imageSource: images.ETHEREUM,
}}
- isSelected={chainId.toString() === providerConfig.chainId}
+ isSelected={
+ chainId.toString() === providerConfig.chainId &&
+ !providerConfig.rpcTarget
+ }
onPress={() => onNetworkChange(MAINNET)}
/>
);
};
- const renderRpcNetworks = () => {
- const rpcList = frequentRpcList.filter(
- ({ chainId }: { chainId: string }) =>
- chainId !== NETWORKS_CHAIN_ID.LINEA_TESTNET,
+ const renderLineaMainnet = () => {
+ const { name: lineaMainnetName, chainId } = Networks['linea-mainnet'];
+ return (
+ onNetworkChange(LINEA_MAINNET)}
+ />
);
+ };
- return rpcList.map(
+ const renderRpcNetworks = () =>
+ frequentRpcList.map(
({
nickname,
rpcUrl,
chainId,
}: {
- nickname: string;
+ nickname?: string;
rpcUrl: string;
- chainId: string;
+ chainId?: number;
}) => {
if (!chainId) return null;
const { name } = { name: nickname || rpcUrl };
@@ -201,16 +175,18 @@ const NetworkSelector = () => {
name,
imageSource: image,
}}
- isSelected={chainId.toString() === providerConfig.chainId}
+ isSelected={
+ chainId.toString() === providerConfig.chainId &&
+ providerConfig.rpcTarget
+ }
onPress={() => onSetRpcTarget(rpcUrl)}
/>
);
},
);
- };
const renderOtherNetworks = () => {
- const getOtherNetworks = () => getAllNetworks().slice(1);
+ const getOtherNetworks = () => getAllNetworks().slice(2);
return getOtherNetworks().map((network) => {
const { name, imageSource, chainId, networkType } = Networks[network];
@@ -231,39 +207,11 @@ const NetworkSelector = () => {
});
};
- const renderNonInfuraNetwork = (
- chainId: string,
- rpcUrl: string,
- nickname: string,
- ) => {
- //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional
- const image = getNetworkImageSource({ chainId: chainId.toString() });
-
- return (
- onSetRpcTarget(rpcUrl)}
- />
- );
- };
-
const goToNetworkSettings = () => {
- navigate('SettingsView', {
- screen: 'SettingsFlow',
- params: {
- screen: 'NetworkSettings',
- params: {
- isFullScreenModal: true,
- },
- },
+ sheetRef.current?.hide(() => {
+ navigate(Routes.ADD_NETWORK, {
+ shouldNetworkSwitchPopToWallet: false,
+ });
});
};
@@ -272,13 +220,9 @@ const NetworkSelector = () => {
{renderMainnet()}
+ {lineaMainnetReleased && renderLineaMainnet()}
{renderRpcNetworks()}
{renderOtherNetworks()}
- {renderNonInfuraNetwork(
- NETWORKS_CHAIN_ID.LINEA_TESTNET,
- LINEA_TESTNET_RPC_URL,
- LINEA_TESTNET_NICKNAME,
- )}
| |
-
+ {enableEthSign && (
+ // display warning if eth_sign is enabled
+
+
+
+ {strings('app_settings.enable_eth_sign_warning')}
+
+
+ )}
+
+
+ this.onEthSignSettingChangeAttempt(!enableEthSign)
+ }
+ style={styles.switchLabel}
+ >
+ {strings(
+ enableEthSign
+ ? 'app_settings.toggleEthSignOn'
+ : 'app_settings.toggleEthSignOff',
+ )}
+
diff --git a/app/components/Views/Settings/AdvancedSettings/index.test.tsx b/app/components/Views/Settings/AdvancedSettings/index.test.tsx
index 8ffa9985ef8..d32dd121a4f 100644
--- a/app/components/Views/Settings/AdvancedSettings/index.test.tsx
+++ b/app/components/Views/Settings/AdvancedSettings/index.test.tsx
@@ -3,25 +3,64 @@ import { shallow } from 'enzyme';
import AdvancedSettings from './';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+import { fireEvent } from '@testing-library/react-native';
+import { strings } from '../../../../../locales/i18n';
+import { Store, AnyAction } from 'redux';
+import Routes from '../../../../constants/navigation/Routes';
+import Engine from '../../../../core/Engine';
const mockStore = configureMockStore();
-const initialState = {
- settings: { showHexData: true },
- engine: {
- backgroundState: {
- PreferencesController: {
- ipfsGateway: 'https://ipfs.io/ipfs/',
- disabledRpcMethodPreferences: {
- eth_sign: false,
+let initialState: any;
+let store: Store;
+const mockNavigate = jest.fn();
+let mockSetDisabledRpcMethodPreference: jest.Mock;
+
+beforeEach(() => {
+ initialState = {
+ settings: { showHexData: true },
+ engine: {
+ backgroundState: {
+ PreferencesController: {
+ ipfsGateway: 'https://ipfs.io/ipfs/',
+ disabledRpcMethodPreferences: {
+ eth_sign: false,
+ },
+ },
+ NetworkController: {
+ providerConfig: { chainId: '1' },
},
},
- NetworkController: {
- providerConfig: { chainId: '1' },
+ },
+ };
+ store = mockStore(initialState);
+ mockNavigate.mockClear();
+ mockSetDisabledRpcMethodPreference.mockClear();
+});
+
+jest.mock('@react-navigation/native', () => {
+ const actualNav = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualNav,
+ navigation: {
+ navigate: mockNavigate,
+ },
+ };
+});
+
+const mockEngine = Engine;
+
+jest.mock('../../../../core/Engine', () => {
+ mockSetDisabledRpcMethodPreference = jest.fn();
+ return {
+ init: () => mockEngine.init({}),
+ context: {
+ PreferencesController: {
+ setDisabledRpcMethodPreference: mockSetDisabledRpcMethodPreference,
},
},
- },
-};
-const store = mockStore(initialState);
+ };
+});
describe('AdvancedSettings', () => {
it('should render correctly', () => {
@@ -32,4 +71,79 @@ describe('AdvancedSettings', () => {
);
expect(wrapper).toMatchSnapshot();
});
+
+ it('should render eth_sign switch off by default with correct label', () => {
+ const { getByLabelText, getByText } = renderWithProvider(
+ ,
+ {
+ state: initialState,
+ },
+ );
+
+ const switchElement = getByLabelText(
+ strings('app_settings.enable_eth_sign'),
+ );
+ expect(switchElement.props.value).toBe(false);
+
+ const textElementOff = getByText(strings('app_settings.toggleEthSignOff'));
+ expect(textElementOff).toBeDefined();
+ });
+
+ it('should render eth_sign switch on with correct label', () => {
+ initialState.engine.backgroundState.PreferencesController.disabledRpcMethodPreferences.eth_sign =
+ true;
+
+ const { getByLabelText, getByText } = renderWithProvider(
+ ,
+ {
+ state: initialState,
+ },
+ );
+
+ const switchElement = getByLabelText(
+ strings('app_settings.enable_eth_sign'),
+ );
+ expect(switchElement.props.value).toBe(true);
+
+ const textElementOn = getByText(strings('app_settings.toggleEthSignOn'));
+ expect(textElementOn).toBeDefined();
+ });
+
+ it('should call navigate to EthSignFriction when eth_sign is switched on', async () => {
+ const { getByLabelText } = renderWithProvider(
+ ,
+ {
+ state: initialState,
+ },
+ );
+
+ const switchElement = getByLabelText(
+ strings('app_settings.enable_eth_sign'),
+ );
+ fireEvent(switchElement, 'onValueChange', true);
+
+ expect(mockNavigate).toBeCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.ETH_SIGN_FRICTION,
+ });
+ expect(mockSetDisabledRpcMethodPreference).not.toBeCalled();
+ });
+
+ it('should directly set setting to off when switched off', async () => {
+ const { getByLabelText } = renderWithProvider(
+ ,
+ {
+ state: initialState,
+ },
+ );
+
+ const switchElement = getByLabelText(
+ strings('app_settings.enable_eth_sign'),
+ );
+ fireEvent(switchElement, 'onValueChange', false);
+ expect(mockNavigate).not.toBeCalled();
+ expect(mockSetDisabledRpcMethodPreference).toBeCalledWith(
+ 'eth_sign',
+ false,
+ );
+ });
});
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
index 2e301a7bb22..d27cfa96f17 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
@@ -287,7 +287,7 @@ class NetworkSettings extends PureComponent {
? strings('app_settings.networks_default_title')
: strings('app_settings.networks_title'),
navigation,
- route?.params?.isFullScreenModal,
+ true,
colors,
),
);
@@ -322,7 +322,11 @@ class NetworkSettings extends PureComponent {
chainId = networkInformation.chainId.toString();
editable = false;
rpcUrl = allNetworksblockExplorerUrl(network);
- ticker = strings('unit.eth');
+ ticker =
+ networkInformation.chainId.toString() !==
+ NETWORKS_CHAIN_ID.LINEA_GOERLI
+ ? strings('unit.eth')
+ : 'LineaETH';
// Override values if UI is updating custom mainnet RPC URL.
if (isCustomMainnet) {
nickname = DEFAULT_MAINNET_CUSTOM_NAME;
@@ -339,9 +343,6 @@ class NetworkSettings extends PureComponent {
networkInformation.rpcPrefs.blockExplorerUrl;
ticker = networkInformation.ticker;
editable = true;
- if (networkInformation.chainId === NETWORKS_CHAIN_ID.LINEA_TESTNET) {
- editable = false;
- }
rpcUrl = network;
}
const initialState =
diff --git a/app/components/Views/Settings/NetworksSettings/index.js b/app/components/Views/Settings/NetworksSettings/index.js
index a56b75aba82..c023c4717fb 100644
--- a/app/components/Views/Settings/NetworksSettings/index.js
+++ b/app/components/Views/Settings/NetworksSettings/index.js
@@ -20,10 +20,12 @@ import Networks, {
getAllNetworks,
getNetworkImageSource,
isDefaultMainnet,
+ isLineaMainnet,
+ shouldShowLineaMainnetNetwork,
} from '../../../../util/networks';
import StyledButton from '../../../UI/StyledButton';
import Engine from '../../../../core/Engine';
-import { MAINNET, NETWORKS_CHAIN_ID, RPC } from '../../../../constants/network';
+import { LINEA_MAINNET, MAINNET, RPC } from '../../../../constants/network';
import FontAwesome from 'react-native-vector-icons/FontAwesome';
import { mockTheme, ThemeContext } from '../../../../util/theme';
import ImageIcons from '../../../UI/ImageIcon';
@@ -39,7 +41,7 @@ import {
} from '../../../../component-library/components/Avatars/Avatar';
import AvatarNetwork from '../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork';
import generateTestId from '../../../../../wdio/utils/generateTestId';
-import { LINEA_TESTNET_RPC_URL } from '../../../../constants/urls';
+import Routes from '../../../../constants/navigation/Routes';
const createStyles = (colors) =>
StyleSheet.create({
@@ -138,6 +140,7 @@ class NetworksSettings extends PureComponent {
state = {
searchString: '',
filteredNetworks: [],
+ lineaMainnetReleased: false,
};
updateNavBar = () => {
@@ -154,6 +157,9 @@ class NetworksSettings extends PureComponent {
};
componentDidMount = () => {
+ const shouldShowLineaMainnet = shouldShowLineaMainnetNetwork();
+
+ this.setState({ lineaMainnetReleased: shouldShowLineaMainnet });
this.updateNavBar();
};
@@ -161,16 +167,16 @@ class NetworksSettings extends PureComponent {
this.updateNavBar();
};
- getOtherNetworks = () => getAllNetworks().slice(1);
+ getOtherNetworks = () => getAllNetworks().slice(2);
onNetworkPress = (network) => {
const { navigation } = this.props;
- navigation.navigate('NetworkSettings', { network });
+ navigation.navigate(Routes.ADD_NETWORK, { network });
};
onAddNetwork = () => {
const { navigation } = this.props;
- navigation.navigate('NetworkSettings');
+ navigation.navigate(Routes.ADD_NETWORK);
};
showRemoveMenu = (network) => {
@@ -217,6 +223,8 @@ class NetworksSettings extends PureComponent {
// Do not change. This logic must check for 'mainnet' and is used for rendering the out of the box mainnet when searching.
isDefaultMainnet(network) ? (
this.renderMainnet()
+ ) : isLineaMainnet(network) ? (
+ this.renderLineaMainnet()
) : (
))}
{name}
- {(network !== LINEA_TESTNET_RPC_URL || !isCustomRPC) && (
+ {!isCustomRPC && (
{
const { frequentRpcList } = this.props;
- return frequentRpcList
- .filter(({ chainId }) => chainId !== NETWORKS_CHAIN_ID.LINEA_TESTNET)
- .map(({ rpcUrl, nickname, chainId }, i) => {
- const { name } = { name: nickname || rpcUrl };
- const image = getNetworkImageSource({ chainId });
- return this.networkElement(name, image, i, rpcUrl, true);
- });
+ return frequentRpcList.map(({ rpcUrl, nickname, chainId }, i) => {
+ const { name } = { name: nickname || rpcUrl };
+ const image = getNetworkImageSource({ chainId });
+ return this.networkElement(name, image, i, rpcUrl, true);
+ });
};
renderRpcNetworksView = () => {
@@ -287,11 +293,7 @@ class NetworksSettings extends PureComponent {
const colors = this.context.colors || mockTheme.colors;
const styles = createStyles(colors);
- if (
- frequentRpcList.filter(
- ({ chainId }) => chainId !== NETWORKS_CHAIN_ID.LINEA_TESTNET,
- ).length > 0
- ) {
+ if (frequentRpcList.length > 0) {
return (
@@ -303,23 +305,6 @@ class NetworksSettings extends PureComponent {
}
};
- renderNonInfuraNetworksView = () => {
- const { frequentRpcList } = this.props;
- if (
- frequentRpcList.filter(
- ({ chainId }) => chainId === NETWORKS_CHAIN_ID.LINEA_TESTNET,
- ).length > 0
- ) {
- return frequentRpcList
- .filter(({ chainId }) => chainId === NETWORKS_CHAIN_ID.LINEA_TESTNET)
- .map(({ rpcUrl, nickname, chainId }, i) => {
- const { name } = { name: nickname || rpcUrl };
- const image = getNetworkImageSource({ chainId });
- return this.networkElement(name, image, i, rpcUrl, true);
- });
- }
- };
-
renderMainnet() {
const { name: mainnetName } = Networks.mainnet;
const colors = this.context.colors || mockTheme.colors;
@@ -349,6 +334,35 @@ class NetworksSettings extends PureComponent {
);
}
+ renderLineaMainnet() {
+ const { name: lineaMainnetName } = Networks['linea-mainnet'];
+ const colors = this.context.colors || mockTheme.colors;
+ const styles = createStyles(colors);
+
+ return (
+
+ this.onNetworkPress(LINEA_MAINNET)}
+ >
+
+
+
+ {lineaMainnetName}
+
+
+
+
+
+ );
+ }
+
handleSearchTextChange = (text) => {
this.setState({ searchString: text });
const defaultNetwork = getAllNetworks().map((network, i) => {
@@ -382,12 +396,10 @@ class NetworksSettings extends PureComponent {
return this.state.filteredNetworks.map((data, i) => {
const { network, chainId, name, color, isCustomRPC } = data;
const image = getNetworkImageSource({ chainId });
- return this.networkElement(
- name,
- image || color,
- i,
- network,
- isCustomRPC,
+ return (
+ // TODO: remove this check when linea mainnet is ready
+ network !== LINEA_MAINNET &&
+ this.networkElement(name, image || color, i, network, isCustomRPC)
);
});
}
@@ -436,12 +448,12 @@ class NetworksSettings extends PureComponent {
{strings('app_settings.mainnet')}
{this.renderMainnet()}
+ {this.state.lineaMainnetReleased && this.renderLineaMainnet()}
{this.renderRpcNetworksView()}
{strings('app_settings.test_network_name')}
{this.renderOtherNetworks()}
- {this.renderNonInfuraNetworksView()}
>
)}
diff --git a/app/components/Views/Settings/SecuritySettings/index.js b/app/components/Views/Settings/SecuritySettings/index.js
index 9bd597fc0e8..07993a687ea 100644
--- a/app/components/Views/Settings/SecuritySettings/index.js
+++ b/app/components/Views/Settings/SecuritySettings/index.js
@@ -61,7 +61,10 @@ import {
} from './Sections';
import Routes from '../../../../constants/navigation/Routes';
import { selectProviderType } from '../../../../selectors/networkController';
-import { SECURITY_PRIVACY_VIEW_ID } from '../../../../../wdio/screen-objects/testIDs/Screens/SecurityPrivacy.testIds';
+import {
+ SECURITY_PRIVACY_MULTI_ACCOUNT_BALANCES_TOGGLE_ID,
+ SECURITY_PRIVACY_VIEW_ID,
+} from '../../../../../wdio/screen-objects/testIDs/Screens/SecurityPrivacy.testIds';
import generateTestId from '../../../../../wdio/utils/generateTestId';
const createStyles = (colors) =>
@@ -178,6 +181,10 @@ const BIOMETRY_CHOICE_STRING = 'biometryChoice';
*/
class Settings extends PureComponent {
static propTypes = {
+ /**
+ * Indicates whether batch balances for multiple accounts is enabled
+ */
+ isMultiAccountBalancesEnabled: PropTypes.bool,
/**
* Called to toggle set party api mode
*/
@@ -525,6 +532,46 @@ class Settings extends PureComponent {
);
};
+ toggleIsMultiAccountBalancesEnabled = (isMultiAccountBalancesEnabled) => {
+ const { PreferencesController } = Engine.context;
+ PreferencesController.setIsMultiAccountBalancesEnabled(
+ isMultiAccountBalancesEnabled,
+ );
+ };
+
+ renderMultiAccountBalancesSection = () => {
+ const { isMultiAccountBalancesEnabled } = this.props;
+ const { styles, colors } = this.getStyles();
+
+ return (
+
+
+ {strings('app_settings.batch_balance_requests_title')}
+
+
+ {strings('app_settings.batch_balance_requests_description')}
+
+
+
+
+
+ );
+ };
+
renderThirdPartySection = () => {
const { thirdPartyApiMode } = this.props;
const { styles, colors } = this.getStyles();
@@ -715,6 +762,7 @@ class Settings extends PureComponent {
{this.renderMetaMetricsSection()}
+ {this.renderMultiAccountBalancesSection()}
{this.renderThirdPartySection()}
{this.renderHistoryModal()}
{this.isMainnet() && this.renderOpenSeaSettings()}
@@ -744,6 +792,9 @@ const mapStateToProps = (state) => ({
passwordHasBeenSet: state.user.passwordSet,
seedphraseBackedUp: state.user.seedphraseBackedUp,
type: selectProviderType(state),
+ isMultiAccountBalancesEnabled:
+ state.engine.backgroundState.PreferencesController
+ .isMultiAccountBalancesEnabled,
});
const mapDispatchToProps = (dispatch) => ({
diff --git a/app/components/Views/Settings/SecuritySettings/index.test.tsx b/app/components/Views/Settings/SecuritySettings/index.test.tsx
index 8d110ccd78b..1de43936b49 100644
--- a/app/components/Views/Settings/SecuritySettings/index.test.tsx
+++ b/app/components/Views/Settings/SecuritySettings/index.test.tsx
@@ -15,6 +15,7 @@ const initialState = {
PreferencesController: {
selectedAddress: '0x',
identities: { '0x': { name: 'Account 1' } },
+ isMultiAccountBalancesEnabled: true,
},
AccountTrackerController: { accounts: {} },
KeyringController: {
diff --git a/app/components/Views/Settings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/__snapshots__/index.test.tsx.snap
index 3e93c8ddfcb..181abc68ffc 100644
--- a/app/components/Views/Settings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/__snapshots__/index.test.tsx.snap
@@ -32,6 +32,6 @@ exports[`Settings should render correctly 1`] = `
}
}
>
-
+
`;
diff --git a/app/components/Views/Settings/index.js b/app/components/Views/Settings/index.js
deleted file mode 100644
index 69f22d18ae3..00000000000
--- a/app/components/Views/Settings/index.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { PureComponent } from 'react';
-import { StyleSheet, ScrollView, InteractionManager } from 'react-native';
-import SettingsDrawer from '../../UI/SettingsDrawer';
-import { getClosableNavigationOptions } from '../../UI/Navbar';
-import { strings } from '../../../../locales/i18n';
-import Analytics from '../../../core/Analytics/Analytics';
-import { MetaMetricsEvents } from '../../../core/Analytics';
-import { connect } from 'react-redux';
-import { ThemeContext, mockTheme } from '../../../util/theme';
-import Routes from '../../../constants/navigation/Routes';
-
-const createStyles = (colors) =>
- StyleSheet.create({
- wrapper: {
- backgroundColor: colors.background.default,
- flex: 1,
- paddingLeft: 18,
- zIndex: 99999999999999,
- },
- });
-
-/**
- * Main view for app configurations
- */
-class Settings extends PureComponent {
- static propTypes = {
- /**
- /* navigation object required to push new views
- */
- navigation: PropTypes.object,
- /**
- * redux flag that indicates if the user
- * completed the seed phrase backup flow
- */
- seedphraseBackedUp: PropTypes.bool,
- };
-
- updateNavBar = () => {
- const { navigation } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- navigation.setOptions(
- getClosableNavigationOptions(
- strings('app_settings.title'),
- strings('navigation.close'),
- navigation,
- colors,
- ),
- );
- };
-
- componentDidMount = () => {
- this.updateNavBar();
- };
-
- componentDidUpdate = () => {
- this.updateNavBar();
- };
-
- onPressGeneral = () => {
- InteractionManager.runAfterInteractions(() =>
- Analytics.trackEvent(MetaMetricsEvents.SETTINGS_GENERAL),
- );
- this.props.navigation.navigate('GeneralSettings');
- };
-
- onPressAdvanced = () => {
- InteractionManager.runAfterInteractions(() =>
- Analytics.trackEvent(MetaMetricsEvents.SETTINGS_ADVANCED),
- );
- this.props.navigation.navigate('AdvancedSettings');
- };
-
- onPressSecurity = () => {
- InteractionManager.runAfterInteractions(() =>
- Analytics.trackEvent(MetaMetricsEvents.SETTINGS_SECURITY_AND_PRIVACY),
- );
- this.props.navigation.navigate('SecuritySettings');
- };
-
- onPressNetworks = () => {
- this.props.navigation.navigate('NetworksSettings');
- };
-
- onPressOnRamp = () => {
- InteractionManager.runAfterInteractions(() =>
- Analytics.trackEvent(MetaMetricsEvents.ONRAMP_SETTINGS_CLICKED),
- );
- this.props.navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.SETTINGS);
- };
-
- onPressExperimental = () => {
- InteractionManager.runAfterInteractions(() =>
- Analytics.trackEvent(MetaMetricsEvents.SETTINGS_EXPERIMENTAL),
- );
- this.props.navigation.navigate('ExperimentalSettings');
- };
-
- onPressInfo = () => {
- InteractionManager.runAfterInteractions(() =>
- Analytics.trackEvent(MetaMetricsEvents.SETTINGS_ABOUT),
- );
- this.props.navigation.navigate('CompanySettings');
- };
-
- onPressContacts = () => {
- this.props.navigation.navigate('ContactsSettings');
- };
-
- render = () => {
- const { seedphraseBackedUp } = this.props;
- const colors = this.context.colors || mockTheme.colors;
- const styles = createStyles(colors);
-
- return (
-
-
-
-
-
-
-
-
-
-
- );
- };
-}
-
-Settings.contextType = ThemeContext;
-
-const mapStateToProps = (state) => ({
- seedphraseBackedUp: state.user.seedphraseBackedUp,
-});
-
-export default connect(mapStateToProps)(Settings);
diff --git a/app/components/Views/Settings/index.test.tsx b/app/components/Views/Settings/index.test.tsx
index d0df9e36728..9dd5b082d30 100644
--- a/app/components/Views/Settings/index.test.tsx
+++ b/app/components/Views/Settings/index.test.tsx
@@ -3,10 +3,24 @@ import { shallow } from 'enzyme';
import Settings from './';
import configureMockStore from 'redux-mock-store';
import { Provider } from 'react-redux';
-
+import renderWithProvider from '../../../util/test/renderWithProvider';
+import {
+ ABOUT_METAMASK_SETTINGS,
+ ADVANCED_SETTINGS,
+ CONTACT_SETTINGS,
+ CONTACTS_SETTINGS,
+ EXPERIMENTAL_SETTINGS,
+ GENERAL_SETTINGS,
+ LOCK_SETTINGS,
+ NETWORKS_SETTINGS,
+ ON_RAMP_SETTINGS,
+ REQUEST_SETTINGS,
+ SECURITY_SETTINGS,
+} from '../../../../wdio/screen-objects/testIDs/Screens/Settings.testIds';
+jest.unmock('react-redux');
const mockStore = configureMockStore();
const initialState = {
- user: { seedphraseBackedUp: true },
+ user: { seedphraseBackedUp: true, passwordSet: true },
privacy: { approvedHosts: [] },
browser: { history: [] },
settings: {
@@ -26,6 +40,20 @@ const initialState = {
},
},
};
+
+const mockSetOptions = jest.fn();
+const mockNavigate = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ setOptions: mockSetOptions,
+ }),
+ };
+});
const store = mockStore(initialState);
describe('Settings', () => {
@@ -37,4 +65,59 @@ describe('Settings', () => {
);
expect(wrapper).toMatchSnapshot();
});
+ it('should render general settings button', () => {
+ const { getByTestId } = renderWithProvider();
+ const generalSettings = getByTestId(GENERAL_SETTINGS);
+ expect(generalSettings).toBeDefined();
+ });
+ it('should render security settings button', () => {
+ const { getByTestId } = renderWithProvider();
+ const securitySettings = getByTestId(SECURITY_SETTINGS);
+ expect(securitySettings).toBeDefined();
+ });
+ it('should render advanced settings button', () => {
+ const { getByTestId } = renderWithProvider();
+ const advancedSettings = getByTestId(ADVANCED_SETTINGS);
+ expect(advancedSettings).toBeDefined();
+ });
+ it('should render contacts settings button', () => {
+ const { getByTestId } = renderWithProvider();
+ const contactsSettings = getByTestId(CONTACTS_SETTINGS);
+ expect(contactsSettings).toBeDefined();
+ });
+ it('should render network settings button', () => {
+ const { getByTestId } = renderWithProvider();
+ const networksSettings = getByTestId(NETWORKS_SETTINGS);
+ expect(networksSettings).toBeDefined();
+ });
+ it('should render feature request button', () => {
+ const { getByTestId } = renderWithProvider();
+ const onRampSettings = getByTestId(ON_RAMP_SETTINGS);
+ expect(onRampSettings).toBeDefined();
+ });
+ it('should render experimental settings button', () => {
+ const { getByTestId } = renderWithProvider();
+ const experimentalSettings = getByTestId(EXPERIMENTAL_SETTINGS);
+ expect(experimentalSettings).toBeDefined();
+ });
+ it('should render about metamask button', () => {
+ const { getByTestId } = renderWithProvider();
+ const aboutMetamask = getByTestId(ABOUT_METAMASK_SETTINGS);
+ expect(aboutMetamask).toBeDefined();
+ });
+ it('should render request feature button', () => {
+ const { getByTestId } = renderWithProvider();
+ const requestFeature = getByTestId(REQUEST_SETTINGS);
+ expect(requestFeature).toBeDefined();
+ });
+ it('should render contact support button', () => {
+ const { getByTestId } = renderWithProvider();
+ const contactSupport = getByTestId(CONTACT_SETTINGS);
+ expect(contactSupport).toBeDefined();
+ });
+ it('should render lock button', () => {
+ const { getByTestId } = renderWithProvider();
+ const lock = getByTestId(LOCK_SETTINGS);
+ expect(lock).toBeDefined();
+ });
});
diff --git a/app/components/Views/Settings/index.tsx b/app/components/Views/Settings/index.tsx
new file mode 100644
index 00000000000..aaa395e5f3c
--- /dev/null
+++ b/app/components/Views/Settings/index.tsx
@@ -0,0 +1,238 @@
+import React, { useCallback, useEffect } from 'react';
+import {
+ StyleSheet,
+ ScrollView,
+ InteractionManager,
+ Alert,
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import SettingsDrawer from '../../UI/SettingsDrawer';
+import { getSettingsNavigationOptions } from '../../UI/Navbar';
+import { strings } from '../../../../locales/i18n';
+import Analytics from '../../../core/Analytics/Analytics';
+import { IMetaMetricsEvent, MetaMetricsEvents } from '../../../core/Analytics';
+import { useSelector } from 'react-redux';
+import { useTheme } from '../../../util/theme';
+import Routes from '../../../constants/navigation/Routes';
+import { Authentication } from '../../../core/';
+import { Colors } from '../../../util/theme/models';
+import {
+ ABOUT_METAMASK_SETTINGS,
+ ADVANCED_SETTINGS,
+ CONTACT_SETTINGS,
+ CONTACTS_SETTINGS,
+ EXPERIMENTAL_SETTINGS,
+ GENERAL_SETTINGS,
+ LOCK_SETTINGS,
+ NETWORKS_SETTINGS,
+ ON_RAMP_SETTINGS,
+ REQUEST_SETTINGS,
+ SECURITY_SETTINGS,
+} from '../../../../wdio/screen-objects/testIDs/Screens/Settings.testIds';
+
+const createStyles = (colors: Colors) =>
+ StyleSheet.create({
+ wrapper: {
+ backgroundColor: colors.background.default,
+ flex: 1,
+ paddingLeft: 18,
+ zIndex: 99999999999999,
+ },
+ });
+
+const Settings = () => {
+ const { colors } = useTheme();
+ const styles = createStyles(colors);
+ const navigation = useNavigation();
+
+ const seedphraseBackedUp = useSelector(
+ (state: any) => state.user.seedphraseBackedUp,
+ );
+ const passwordSet = useSelector((state: any) => state.user.passwordSet);
+
+ const updateNavBar = useCallback(() => {
+ navigation.setOptions(
+ getSettingsNavigationOptions(strings('app_settings.title'), colors),
+ );
+ }, [navigation, colors]);
+
+ useEffect(() => {
+ updateNavBar();
+ }, [updateNavBar]);
+
+ const trackEvent = (event: IMetaMetricsEvent) => {
+ InteractionManager.runAfterInteractions(() => {
+ Analytics.trackEvent(event);
+ });
+ };
+
+ const onPressGeneral = () => {
+ trackEvent(MetaMetricsEvents.SETTINGS_GENERAL);
+ navigation.navigate('GeneralSettings');
+ };
+
+ const onPressAdvanced = () => {
+ trackEvent(MetaMetricsEvents.SETTINGS_ADVANCED);
+ navigation.navigate('AdvancedSettings');
+ };
+
+ const onPressSecurity = () => {
+ trackEvent(MetaMetricsEvents.SETTINGS_SECURITY_AND_PRIVACY);
+ navigation.navigate('SecuritySettings');
+ };
+
+ const onPressNetworks = () => {
+ navigation.navigate('NetworksSettings');
+ };
+
+ const onPressOnRamp = () => {
+ trackEvent(MetaMetricsEvents.ONRAMP_SETTINGS_CLICKED);
+ navigation.navigate(Routes.FIAT_ON_RAMP_AGGREGATOR.SETTINGS);
+ };
+
+ const onPressExperimental = () => {
+ trackEvent(MetaMetricsEvents.SETTINGS_EXPERIMENTAL);
+ navigation.navigate('ExperimentalSettings');
+ };
+
+ const onPressInfo = () => {
+ trackEvent(MetaMetricsEvents.SETTINGS_ABOUT);
+ navigation.navigate('CompanySettings');
+ };
+
+ const onPressContacts = () => {
+ navigation.navigate('ContactsSettings');
+ };
+
+ const goToBrowserUrl = (url: string, title: string) => {
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url,
+ title,
+ },
+ });
+ };
+
+ const submitFeedback = () => {
+ trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_SEND_FEEDBACK);
+ goToBrowserUrl(
+ 'https://community.metamask.io/c/feature-requests-ideas/',
+ strings('app_settings.request_feature'),
+ );
+ };
+
+ const showHelp = () => {
+ goToBrowserUrl(
+ 'https://support.metamask.io',
+ strings('app_settings.contact_support'),
+ );
+ trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_GET_HELP);
+ };
+
+ const onPressLock = async () => {
+ await Authentication.lockApp();
+ if (!passwordSet) {
+ navigation.navigate('OnboardingRootNav', {
+ screen: Routes.ONBOARDING.NAV,
+ params: { screen: 'Onboarding' },
+ });
+ } else {
+ navigation.replace(Routes.ONBOARDING.LOGIN, { locked: true });
+ }
+ };
+
+ const lock = () => {
+ Alert.alert(
+ strings('drawer.lock_title'),
+ '',
+ [
+ {
+ text: strings('drawer.lock_cancel'),
+ onPress: () => null,
+ style: 'cancel',
+ },
+ {
+ text: strings('drawer.lock_ok'),
+ onPress: onPressLock,
+ },
+ ],
+ { cancelable: false },
+ );
+ trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_LOGOUT);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/app/components/Views/Wallet/index.test.tsx b/app/components/Views/Wallet/index.test.tsx
index a4e9217e5b3..a6489f55bc3 100644
--- a/app/components/Views/Wallet/index.test.tsx
+++ b/app/components/Views/Wallet/index.test.tsx
@@ -153,4 +153,9 @@ describe('Wallet', () => {
render(Wallet);
expect(ScrollableTabView).toHaveBeenCalled();
});
+ it('should render fox icon', () => {
+ render(Wallet);
+ const foxIcon = screen.getByTestId('fox-icon');
+ expect(foxIcon).toBeDefined();
+ });
});
diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx
index 7f6b3d59e1e..0e3a70c0cc3 100644
--- a/app/components/Views/Wallet/index.tsx
+++ b/app/components/Views/Wallet/index.tsx
@@ -1,10 +1,4 @@
-import React, {
- useEffect,
- useRef,
- useCallback,
- useContext,
- useMemo,
-} from 'react';
+import React, { useEffect, useRef, useCallback, useMemo } from 'react';
import {
InteractionManager,
ActivityIndicator,
@@ -28,7 +22,6 @@ import { MetaMetricsEvents } from '../../../core/Analytics';
import { getTicker } from '../../../util/transactions';
import OnboardingWizard from '../../UI/OnboardingWizard';
import ErrorBoundary from '../ErrorBoundary';
-import { DrawerContext } from '../../Nav/Main/MainNavigator';
import { useTheme } from '../../../util/theme';
import { shouldShowWhatsNewModal } from '../../../util/onboarding';
import Logger from '../../../util/Logger';
@@ -81,7 +74,6 @@ const createStyles = ({ colors, typography }: Theme) =>
*/
const Wallet = ({ navigation }: any) => {
const { navigate } = useNavigation();
- const { drawerRef } = useContext(DrawerContext);
const walletRef = useRef(null);
const theme = useTheme();
const styles = createStyles(theme);
@@ -150,7 +142,7 @@ const Wallet = ({ navigation }: any) => {
/**
* Callback to trigger when pressing the navigation title.
*/
- const onTitlePress = () => {
+ const onTitlePress = useCallback(() => {
navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
screen: Routes.SHEET.NETWORK_SELECTOR,
});
@@ -160,7 +152,7 @@ const Wallet = ({ navigation }: any) => {
chain_id: networkProvider.chainId,
},
);
- };
+ }, [navigate, networkProvider.chainId]);
const { colors: themeColors } = useTheme();
useEffect(() => {
@@ -215,7 +207,6 @@ const Wallet = ({ navigation }: any) => {
networkImageSource,
onTitlePress,
navigation,
- drawerRef,
themeColors,
),
);
diff --git a/app/components/Views/WalletActions/WalletActions.test.tsx b/app/components/Views/WalletActions/WalletActions.test.tsx
index a1705294b09..7ccdc915449 100644
--- a/app/components/Views/WalletActions/WalletActions.test.tsx
+++ b/app/components/Views/WalletActions/WalletActions.test.tsx
@@ -55,9 +55,18 @@ jest.mock('@react-navigation/native', () => {
};
});
-jest.mock('react-native-safe-area-context', () => ({
- useSafeAreaInsets: () => ({ top: 0, left: 0, right: 0, bottom: 0 }),
-}));
+jest.mock('react-native-safe-area-context', () => {
+ const inset = { top: 0, right: 0, bottom: 0, left: 0 };
+ const frame = { width: 0, height: 0, x: 0, y: 0 };
+ return {
+ SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children),
+ SafeAreaConsumer: jest
+ .fn()
+ .mockImplementation(({ children }) => children(inset)),
+ useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
+ useSafeAreaFrame: jest.fn().mockImplementation(() => frame),
+ };
+});
describe('WalletActions', () => {
afterEach(() => {
diff --git a/app/components/hooks/useAccounts/useAccounts.ts b/app/components/hooks/useAccounts/useAccounts.ts
index 2695c5a5bf4..64af1ade5bb 100644
--- a/app/components/hooks/useAccounts/useAccounts.ts
+++ b/app/components/hooks/useAccounts/useAccounts.ts
@@ -37,6 +37,7 @@ const useAccounts = ({
const [accounts, setAccounts] = useState([]);
const [ensByAccountAddress, setENSByAccountAddress] =
useState({});
+
const identities = useSelector(
(state: any) =>
state.engine.backgroundState.PreferencesController.identities,
@@ -61,6 +62,12 @@ const useAccounts = ({
);
const ticker = useSelector(selectTicker);
+ const isMultiAccountBalancesEnabled = useSelector(
+ (state: any) =>
+ state.engine.backgroundState.PreferencesController
+ .isMultiAccountBalancesEnabled,
+ );
+
const fetchENSNames = useCallback(
async ({
flattenedAccounts,
@@ -116,6 +123,7 @@ const useAccounts = ({
);
const getAccounts = useCallback(() => {
+ if (!isMountedRef.current) return;
// Keep track of the Y position of account item. Used for scrolling purposes.
let yOffset = 0;
let selectedIndex = 0;
@@ -149,6 +157,7 @@ const useAccounts = ({
const balanceTicker = getTicker(ticker);
const balanceLabel = `${balanceFiat}\n${balanceETH} ${balanceTicker}`;
const balanceError = checkBalanceError?.(balanceWeiHex);
+ const isBalanceAvailable = isMultiAccountBalancesEnabled || isSelected;
const mappedAccount: Account = {
name,
address: checksummedAddress,
@@ -157,7 +166,7 @@ const useAccounts = ({
isSelected,
// TODO - Also fetch assets. Reference AccountList component.
// assets
- assets: { fiatBalance: balanceLabel },
+ assets: isBalanceAvailable && { fiatBalance: balanceLabel },
balanceError,
};
result.push(mappedAccount);
@@ -172,6 +181,7 @@ const useAccounts = ({
}
return result;
}, []);
+
setAccounts(flattenedAccounts);
fetchENSNames({ flattenedAccounts, startingIndex: selectedIndex });
/* eslint-disable-next-line */
@@ -184,6 +194,7 @@ const useAccounts = ({
currentCurrency,
ticker,
checkBalanceError,
+ isMultiAccountBalancesEnabled,
]);
useEffect(() => {
@@ -199,7 +210,10 @@ const useAccounts = ({
};
}, [getAccounts, isLoading]);
- return { accounts, ensByAccountAddress };
+ return {
+ accounts,
+ ensByAccountAddress,
+ };
};
export default useAccounts;
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 291b5e3d06b..67e3fd153f1 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -3,6 +3,7 @@ const Routes = {
BROWSER_TAB_HOME: 'BrowserTabHome',
BROWSER_URL_MODAL: 'BrowserUrlModal',
BROWSER_VIEW: 'BrowserView',
+ SETTINGS_VIEW: 'SettingsView',
FIAT_ON_RAMP_AGGREGATOR: {
ID: 'FiatOnRampAggregator',
GET_STARTED: 'GetStarted',
@@ -64,6 +65,7 @@ const Routes = {
ACCOUNT_PERMISSIONS: 'AccountPermissions',
NETWORK_SELECTOR: 'NetworkSelector',
ACCOUNT_ACTIONS: 'AccountActions',
+ ETH_SIGN_FRICTION: 'SettingsAdvancedEthSignFriction',
},
BROWSER: {
HOME: 'BrowserTabHome',
@@ -83,6 +85,7 @@ const Routes = {
WALLET_RESTORED: 'WalletRestored',
WALLET_RESET_NEEDED: 'WalletResetNeeded',
},
+ ADD_NETWORK: 'AddNetwork',
SWAPS: 'Swaps',
};
diff --git a/app/constants/network.js b/app/constants/network.js
index 5a0b51b42e7..bc3f888f6b6 100644
--- a/app/constants/network.js
+++ b/app/constants/network.js
@@ -2,12 +2,12 @@ export const MAINNET = 'mainnet';
export const HOMESTEAD = 'homestead';
export const GOERLI = 'goerli';
export const SEPOLIA = 'sepolia';
+export const LINEA_GOERLI = 'linea-goerli';
+export const LINEA_MAINNET = 'linea-mainnet';
export const RPC = 'rpc';
export const NO_RPC_BLOCK_EXPLORER = 'NO_BLOCK_EXPLORER';
export const PRIVATENETWORK = 'PRIVATENETWORK';
export const DEFAULT_MAINNET_CUSTOM_NAME = 'Ethereum Main Custom';
-export const LINEA_TESTNET_NICKNAME = 'Linea Goerli Test Network';
-export const LINEA_TESTNET_TICKER = 'LineaETH';
/**
* @enum {string}
@@ -23,6 +23,7 @@ export const NETWORKS_CHAIN_ID = {
CELO: '42220',
HARMONY: '1666600000',
SEPOLIA: '11155111',
- LINEA_TESTNET: '59140',
+ LINEA_GOERLI: '59140',
GOERLI: '5',
+ LINEA_MAINNET: '59144',
};
diff --git a/app/constants/on-ramp.ts b/app/constants/on-ramp.ts
index 16673708301..25a31bb53f4 100644
--- a/app/constants/on-ramp.ts
+++ b/app/constants/on-ramp.ts
@@ -23,7 +23,6 @@ export const FORMATTED_NETWORK_NAMES = {
[NETWORKS_CHAIN_ID.AVAXCCHAIN]: 'Avalanche',
[NETWORKS_CHAIN_ID.CELO]: 'Celo',
[NETWORKS_CHAIN_ID.FANTOM]: 'Fantom',
- [NETWORKS_CHAIN_ID.LINEA_TESTNET]: 'Linea Goerli test network',
} as const;
export const NATIVE_ADDRESS = '0x0000000000000000000000000000000000000000';
diff --git a/app/constants/test-ids.js b/app/constants/test-ids.js
index 421fcf81980..89167a62d4d 100644
--- a/app/constants/test-ids.js
+++ b/app/constants/test-ids.js
@@ -45,7 +45,7 @@ export const APPROVE_NETWORK_DISPLAY_NAME_ID =
export const REMOVE_NETWORK_ID = 'remove-network-button';
export const ADD_NETWORKS_ID = 'add-network-button';
-export const ADD_CUSTOM_RPC_NETWORK_BUTTON_ID = 'add-network-button';
+export const ADD_CUSTOM_RPC_NETWORK_BUTTON_ID = 'add-custom-network-button';
export const RPC_VIEW_CONTAINER_ID = 'new-rpc-screen';
export const NEW_NETWORK_ADDED_CLOSE_BUTTON_ID = 'close-network-button';
@@ -78,6 +78,7 @@ export const CONFIRMATION_MODAL_DANGER_BUTTON_ID =
export const TOKEN_AVATAR_IMAGE_ID = 'token-avatar-image';
export const STACKED_AVATARS_OVERFLOW_COUNTER =
'stacked-avatar-overflow-counter';
+export const BROWSER_WEBVIEW_ID = 'browser-webview';
// LoginOptionsSwitch
export const LOGIN_WITH_BIOMETRICS_SWITCH = 'login-with-biometrics-switch';
@@ -87,3 +88,16 @@ export const TURN_OFF_REMEMBER_ME_MODAL = 'TurnOffRememberMeConfirm';
export const PROTECT_YOUR_ACCOUNT_SCREEN = 'protect-your-account-screen';
export const MANUAL_BACKUP_STEP_2_CONTINUE_BUTTON =
'manual-backup-step-2-continue-button';
+
+// Signature Modal
+export const SIGNATURE_MODAL_ETH_ID = 'eth-signature-request';
+export const SIGNATURE_MODAL_PERSONAL_ID = 'personal-signature-request';
+export const SIGNATURE_MODAL_TYPED_ID = 'typed-signature-request';
+export const SIGNATURE_MODAL_SIGN_BUTTON_ID =
+ 'request-signature-confirm-button';
+export const SIGNATURE_MODAL_CANCEL_BUTTON_ID =
+ 'request-signature-cancel-button';
+
+// Advanced Settings
+export const ADVANCED_SETTINGS_CONTAINER_ID = 'advanced-settings';
+export const ETH_SIGN_SWITCH_ID = 'eth-sign-switch';
diff --git a/app/constants/urls.ts b/app/constants/urls.ts
index 0a26e70bd23..fd1b6c054fa 100644
--- a/app/constants/urls.ts
+++ b/app/constants/urls.ts
@@ -9,12 +9,16 @@ export const KEEP_SRP_SAFE_URL =
'https://metamask.zendesk.com/hc/en-us/articles/4407169552667-Scammers-and-Phishers-Rugpulls-and-airdrop-scams';
export const LEARN_MORE_URL =
'https://metamask.zendesk.com/hc/en-us/articles/360015489591-Basic-Safety-and-Security-Tips-for-MetaMask';
+export const WHY_TRANSACTION_TAKE_TIME_URL =
+ 'https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172';
// Policies
export const CONSENSYS_PRIVACY_POLICY = 'https://consensys.net/privacy-policy/';
// Keystone
export const KEYSTONE_SUPPORT = 'https://keyst.one/mmm';
+export const KEYSTONE_LEARN_MORE =
+ 'https://keyst.one/metamask?rfsn=6088257.656b3e9&utm_source=refersion&utm_medium=affiliate&utm_campaign=6088257.656b3e9';
export const KEYSTONE_SUPPORT_VIDEO = 'https://keyst.one/mmmvideo';
// MixPanel
@@ -23,10 +27,9 @@ export const MIXPANEL_ENDPOINT_BASE_URL = 'https://mixpanel.com/api/app';
// Network
export const CHAINLIST_URL = 'https://chainlist.wtf';
export const MM_ETHERSCAN_URL = 'https://etherscamdb.info/domain/meta-mask.com';
-export const LINEA_TESTNET_BLOCK_EXPLORER =
- 'https://explorer.goerli.linea.build';
-export const LINEA_TESTNET_RPC_URL = 'https://rpc.goerli.linea.build';
-
+export const LINEA_GOERLI_BLOCK_EXPLORER = 'https://goerli.lineascan.build';
+export const LINEA_MAINNET_BLOCK_EXPLORER = 'https://lineascan.build';
+export const LINEA_MAINNET_RPC_URL = `https://linea-mainnet.infura.io/v3/${process.env.MM_INFURA_PROJECT_ID}`;
// Phishing
export const MM_PHISH_DETECT_URL =
'https://github.com/metamask/eth-phishing-detect';
diff --git a/app/core/Analytics/Analytics.js b/app/core/Analytics/Analytics.js
index 637f798f785..8131db784b6 100644
--- a/app/core/Analytics/Analytics.js
+++ b/app/core/Analytics/Analytics.js
@@ -32,6 +32,7 @@ const USER_PROFILE_PROPERTY = {
OFF: 'OFF',
AUTHENTICATION_TYPE: 'Authentication Type',
TOKEN_DETECTION: 'token_detection_enable',
+ MULTI_ACCOUNT_BALANCE: 'Batch account balance requests',
};
/**
@@ -119,6 +120,13 @@ class Analytics {
? USER_PROFILE_PROPERTY.ON
: USER_PROFILE_PROPERTY.OFF,
);
+ // Track multi account balance toggle
+ RCTAnalytics.setUserProfileProperty(
+ USER_PROFILE_PROPERTY.MULTI_ACCOUNT_BALANCE,
+ preferencesController.isMultiAccountBalancesEnabled
+ ? USER_PROFILE_PROPERTY.ON
+ : USER_PROFILE_PROPERTY.OFF,
+ );
};
/**
diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts
index e664dff5aff..51d3df284e8 100644
--- a/app/core/Analytics/MetaMetrics.events.ts
+++ b/app/core/Analytics/MetaMetrics.events.ts
@@ -152,6 +152,10 @@ enum EVENT_NAME {
// Security & Privacy Settings
VIEW_SECURITY_SETTINGS = 'Views Security & Privacy',
+ // Settings
+ SETTINGS_VIEWED = 'Settings Viewed',
+ SETTINGS_UPDATED = 'Settings Updated',
+
// Reveal SRP
REVEAL_SRP_CTA = 'Clicks Reveal Secret Recovery Phrase',
REVEAL_SRP_SCREEN = 'Views Reveal Secret Recovery Phrase',
@@ -345,6 +349,10 @@ enum ACTIONS {
SWAP = 'Swap',
PERMISSION_NEW_ACCOUNT = 'Connected new account(s)',
PERMISSION_REVOKE_ACCOUNT = 'Revoked account(s)',
+ ADVANCED_SETTINGS_ETH_SIGN_FRICTION_FIRST_STEP = 'eth_sign_checkbox_seen',
+ ADVANCED_SETTINGS_ETH_SIGN_FRICTION_SECOND_STEP = 'eth_sign_input_seen',
+ ADVANCED_SETTINGS_ETH_SIGN_ENABLED = 'eth_sign_enabled',
+ ADVANCED_SETTINGS_ETH_SIGN_DISABLED = 'eth_sign_disabled',
}
const events = {
@@ -670,6 +678,24 @@ const events = {
// Edit account name
ACCOUNT_RENAMED: generateOpt(EVENT_NAME.ACCOUNT_RENAMED),
+
+ // Settings
+ SETTINGS_ADVANCED_ETH_SIGN_FRICTION_FIRST_STEP_VIEWED: generateOpt(
+ EVENT_NAME.SETTINGS_VIEWED,
+ ACTIONS.ADVANCED_SETTINGS_ETH_SIGN_FRICTION_FIRST_STEP,
+ ),
+ SETTINGS_ADVANCED_ETH_SIGN_FRICTION_SECOND_STEP_VIEWED: generateOpt(
+ EVENT_NAME.SETTINGS_VIEWED,
+ ACTIONS.ADVANCED_SETTINGS_ETH_SIGN_FRICTION_SECOND_STEP,
+ ),
+ SETTINGS_ADVANCED_ETH_SIGN_ENABLED: generateOpt(
+ EVENT_NAME.SETTINGS_UPDATED,
+ ACTIONS.ADVANCED_SETTINGS_ETH_SIGN_ENABLED,
+ ),
+ SETTINGS_ADVANCED_ETH_SIGN_DISABLED: generateOpt(
+ EVENT_NAME.SETTINGS_UPDATED,
+ ACTIONS.ADVANCED_SETTINGS_ETH_SIGN_DISABLED,
+ ),
};
/**
@@ -760,7 +786,6 @@ enum DESCRIPTION {
// Dapp Interactions
DAPP_APPROVE_SCREEN_APPROVE = 'Approve',
DAPP_APPROVE_SCREEN_CANCEL = 'Cancel',
- DAPP_APPROVE_SCREEN_EDIT_PERMISSION = 'Edit permission',
DAPP_APPROVE_SCREEN_EDIT_FEE = 'Edit tx fee',
DAPP_APPROVE_SCREEN_VIEW_DETAILS = 'View tx details',
PAYMENTS_SELECTS_DEBIT_OR_ACH = 'Selects debit card or bank account as payment method',
@@ -1127,11 +1152,6 @@ const legacyMetaMetricsEvents = {
ACTIONS.APPROVE_REQUEST,
DESCRIPTION.DAPP_APPROVE_SCREEN_CANCEL,
),
- DAPP_APPROVE_SCREEN_EDIT_PERMISSION: generateOpt(
- EVENT_NAME.DAPP_INTERACTIONS,
- ACTIONS.APPROVE_REQUEST,
- DESCRIPTION.DAPP_APPROVE_SCREEN_EDIT_PERMISSION,
- ),
DAPP_APPROVE_SCREEN_EDIT_FEE: generateOpt(
EVENT_NAME.DAPP_INTERACTIONS,
ACTIONS.APPROVE_REQUEST,
diff --git a/app/core/AppConstants.js b/app/core/AppConstants.js
index fb85b38ce64..c913d4565cb 100644
--- a/app/core/AppConstants.js
+++ b/app/core/AppConstants.js
@@ -88,6 +88,8 @@ export default {
MM_FAUCET: 'https://faucet.metamask.io/',
WHY_TRANSACTION_TAKE_TIME:
'https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172',
+ WHAT_IS_ETH_SIGN_AND_WHY_IS_IT_A_RISK:
+ 'https://support.metamask.io/hc/articles/14764161421467',
},
ERRORS: {
INFURA_BLOCKED_MESSAGE:
diff --git a/app/core/Engine.ts b/app/core/Engine.ts
index 47070fd93f6..cd5ab540679 100644
--- a/app/core/Engine.ts
+++ b/app/core/Engine.ts
@@ -83,6 +83,22 @@ class Engine {
constructor(initialState = {}, initialKeyringState) {
if (!Engine.instance) {
this.controllerMessenger = new ControllerMessenger();
+
+ const approvalController = new ApprovalController({
+ messenger: this.controllerMessenger.getRestricted({
+ name: 'ApprovalController',
+ }),
+ showApprovalRequest: () => null,
+ typesExcludedFromRateLimiting: [
+ // TODO: Replace with ApprovalType enum from @metamask/controller-utils when breaking change is fixed
+ 'eth_sign',
+ 'personal_sign',
+ 'eth_signTypedData',
+ 'transaction',
+ 'wallet_watchAsset',
+ ],
+ });
+
const preferencesController = new PreferencesController(
{},
{
@@ -163,6 +179,10 @@ class Engine {
provider: networkController.provider,
chainId: networkController.state.providerConfig.chainId,
},
+ messenger: this.controllerMessenger.getRestricted({
+ name: 'TokensController',
+ allowedActions: [`${approvalController.name}:addRequest`],
+ }),
getERC20TokenName: assetsContractController.getERC20TokenName.bind(
assetsContractController,
),
@@ -209,21 +229,6 @@ class Engine {
'https://gas-api.metaswap.codefi.network/networks//suggestedGasFees',
});
- const approvalController = new ApprovalController({
- messenger: this.controllerMessenger.getRestricted({
- name: 'ApprovalController',
- }),
- showApprovalRequest: () => null,
- typesExcludedFromRateLimiting: [
- // TODO: Replace with ApprovalType enum from @metamask/controller-utils when breaking change is fixed
- 'eth_sign',
- 'personal_sign',
- 'eth_signTypedData',
- 'transaction',
- 'wallet_watchAsset',
- ],
- });
-
const phishingController = new PhishingController();
phishingController.maybeUpdateState();
@@ -269,6 +274,9 @@ class Engine {
onPreferencesStateChange: (listener) =>
preferencesController.subscribe(listener),
getIdentities: () => preferencesController.state.identities,
+ getSelectedAddress: () => preferencesController.state.selectedAddress,
+ getMultiAccountBalancesEnabled: () =>
+ preferencesController.state.isMultiAccountBalancesEnabled,
}),
new AddressBookController(),
assetsContractController,
@@ -366,6 +374,14 @@ class Engine {
listener,
),
getProvider: () => networkController.provider,
+ messenger: this.controllerMessenger.getRestricted({
+ name: 'TransactionController',
+ allowedActions: [
+ `${approvalController.name}:addRequest`,
+ `${approvalController.name}:acceptRequest`,
+ `${approvalController.name}:rejectRequest`,
+ ],
+ }),
}),
new SwapsController(
{
@@ -740,7 +756,6 @@ class Engine {
allTokens: {},
ignoredTokens: [],
tokens: [],
- suggestedAssets: [],
});
NftController.update({
allNftContracts: {},
@@ -753,7 +768,6 @@ class Engine {
allIgnoredTokens: {},
ignoredTokens: [],
tokens: [],
- suggestedAssets: [],
});
TokenBalancesController.update({ contractBalances: {} });
diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts
index d1eb4626e7c..3c146671ed7 100644
--- a/app/core/RPCMethods/RPCMethodMiddleware.ts
+++ b/app/core/RPCMethods/RPCMethodMiddleware.ts
@@ -36,6 +36,8 @@ export enum ApprovalTypes {
ETH_SIGN = 'eth_sign',
PERSONAL_SIGN = 'personal_sign',
ETH_SIGN_TYPED_DATA = 'eth_signTypedData',
+ WATCH_ASSET = 'wallet_watchAsset',
+ TRANSACTION = 'transaction',
}
interface RPCMethodsMiddleParameters {
@@ -632,36 +634,24 @@ export const getRpcMethodMiddleware = ({
const { chainId } = NetworkController.state?.providerConfig || {};
checkTabActive();
- try {
- // Check if token exists on wallet's active network.
- const isTokenOnNetwork = await isSmartContractAddress(
- address,
- chainId,
- );
- if (!isTokenOnNetwork) {
- throw new Error(TOKEN_NOT_SUPPORTED_FOR_NETWORK);
- }
- const permittedAccounts = await getPermittedAccounts(hostname);
- // This should return the current active account on the Dapp.
- const selectedAddress =
- Engine.context.PreferencesController.state.selectedAddress;
- // Fallback to wallet address if there is no connected account to Dapp.
- const interactingAddress = permittedAccounts?.[0] || selectedAddress;
- const watchAssetResult = await TokensController.watchAsset(
- { address, symbol, decimals, image },
- type,
- interactingAddress,
- );
- await watchAssetResult.result;
- res.result = true;
- } catch (error) {
- if (
- (error as Error).message === 'User rejected to watch the asset.'
- ) {
- throw ethErrors.provider.userRejectedRequest();
- }
- throw error;
+
+ // Check if token exists on wallet's active network.
+ const isTokenOnNetwork = await isSmartContractAddress(address, chainId);
+ if (!isTokenOnNetwork) {
+ throw new Error(TOKEN_NOT_SUPPORTED_FOR_NETWORK);
}
+ const permittedAccounts = await getPermittedAccounts(hostname);
+ // This should return the current active account on the Dapp.
+ const selectedAddress =
+ Engine.context.PreferencesController.state.selectedAddress;
+ // Fallback to wallet address if there is no connected account to Dapp.
+ const interactingAddress = permittedAccounts?.[0] || selectedAddress;
+ await TokensController.watchAsset(
+ { address, symbol, decimals, image },
+ type,
+ safeToChecksumAddress(interactingAddress),
+ );
+ res.result = true;
},
metamask_removeFavorite: async () => {
diff --git a/app/images/image-icons.js b/app/images/image-icons.js
index a205f43f350..5a71b1bf95e 100644
--- a/app/images/image-icons.js
+++ b/app/images/image-icons.js
@@ -8,8 +8,9 @@ import ETHEREUM from './eth-logo-new.png';
import BNB from './binance.png';
import AVAX from './avalanche.png';
import GOERLI from './goerli-logo-dark.png';
-import LINEA_TESTNET from './linea-logo-dark.png';
+import LINEA_GOERLI from './linea-testnet-logo.png';
import SEPOLIA from './sepolia-logo-dark.png';
+import LINEA_MAINNET from './linea-mainnet-logo.png';
export default {
PALM,
@@ -22,6 +23,7 @@ export default {
AETH,
AVAX,
GOERLI,
- LINEA_TESTNET,
+ 'LINEA-GOERLI': LINEA_GOERLI,
SEPOLIA,
+ 'LINEA-MAINNET': LINEA_MAINNET,
};
diff --git a/app/images/linea-logo-dark.png b/app/images/linea-logo-dark.png
deleted file mode 100644
index 5bdff1e44a9..00000000000
Binary files a/app/images/linea-logo-dark.png and /dev/null differ
diff --git a/app/images/linea-mainnet-logo.png b/app/images/linea-mainnet-logo.png
new file mode 100644
index 00000000000..23a9f9d1a51
Binary files /dev/null and b/app/images/linea-mainnet-logo.png differ
diff --git a/app/images/linea-testnet-logo.png b/app/images/linea-testnet-logo.png
new file mode 100644
index 00000000000..d9f78720bb1
Binary files /dev/null and b/app/images/linea-testnet-logo.png differ
diff --git a/app/reducers/modals/index.js b/app/reducers/modals/index.js
index 70a9beba087..5a7c33c33f9 100644
--- a/app/reducers/modals/index.js
+++ b/app/reducers/modals/index.js
@@ -5,7 +5,6 @@ const initialState = {
receiveModalVisible: false,
receiveAsset: undefined,
dappTransactionModalVisible: false,
- approveModalVisible: false,
};
const modalsReducer = (state = initialState, action) => {
@@ -42,17 +41,6 @@ const modalsReducer = (state = initialState, action) => {
? !state.dappTransactionModalVisible
: action.show,
};
- case 'TOGGLE_APPROVE_MODAL':
- if (action.show === false) {
- return {
- ...state,
- approveModalVisible: false,
- };
- }
- return {
- ...state,
- approveModalVisible: !state.approveModalVisible,
- };
case 'TOGGLE_INFO_NETWORK_MODAL':
if (action.show === false) {
return {
diff --git a/app/selectors/networkController.ts b/app/selectors/networkController.ts
index bbccd262e08..2fdea32e284 100644
--- a/app/selectors/networkController.ts
+++ b/app/selectors/networkController.ts
@@ -1,10 +1,6 @@
import { createSelector } from 'reselect';
import { EngineState } from './types';
-import {
- ProviderConfig,
- NetworkState,
- NetworkType,
-} from '@metamask/network-controller';
+import { ProviderConfig, NetworkState } from '@metamask/network-controller';
const selectNetworkControllerState = (state: EngineState) =>
state?.engine?.backgroundState?.NetworkController;
@@ -24,7 +20,7 @@ export const selectChainId = createSelector(
selectProviderConfig,
(providerConfig: ProviderConfig) => providerConfig?.chainId,
);
-export const selectProviderType: NetworkType = createSelector(
+export const selectProviderType = createSelector(
selectProviderConfig,
(providerConfig: ProviderConfig) => providerConfig?.type,
);
diff --git a/app/store/migrations.js b/app/store/migrations.js
index 98b02ea4f67..ac7bcb3d2cf 100644
--- a/app/store/migrations.js
+++ b/app/store/migrations.js
@@ -421,6 +421,11 @@ export const migrations = {
}
return state;
},
+ 18: (state) => {
+ if (state.engine.backgroundState.TokensController.suggestedAssets) {
+ delete state.engine.backgroundState.TokensController.suggestedAssets;
+ }
+ },
};
-export const version = 17;
+export const version = 18;
diff --git a/app/util/dappTransactions/index.test.ts b/app/util/dappTransactions/index.test.ts
index b09ca8c76b4..30bc9789278 100644
--- a/app/util/dappTransactions/index.test.ts
+++ b/app/util/dappTransactions/index.test.ts
@@ -1,10 +1,12 @@
+import { BN } from 'ethereumjs-util';
+
import { strings } from '../../../locales/i18n';
+import Engine from '../../core/Engine';
import {
validateCollectibleOwnership,
validateEtherAmount,
validateTokenAmount,
} from '.';
-import { BN } from 'ethereumjs-util';
const TEST_VALUE = new BN('0');
const TEST_INVALID_VALUE = '0';
@@ -13,6 +15,7 @@ const TEST_INVALID_FROM = null;
const TEST_GAS = new BN('0');
const TEST_INVALID_GAS = null;
const TEST_SELECTED_ASSET = { address: '0x', decimals: '0', symbol: 'ETH' };
+const TEST_SELECTED_ADDRESS = '0x0';
const TEST_CONTRACT_BALANCES = {};
describe('Dapp Transactions utils :: validateEtherAmount', () => {
@@ -38,6 +41,7 @@ describe('Dapp Transactions utils :: validateTokenAmount', () => {
TEST_GAS,
TEST_FROM,
TEST_SELECTED_ASSET,
+ TEST_SELECTED_ADDRESS,
TEST_CONTRACT_BALANCES,
false,
),
@@ -51,6 +55,7 @@ describe('Dapp Transactions utils :: validateTokenAmount', () => {
TEST_INVALID_GAS as unknown as BN,
TEST_FROM,
TEST_SELECTED_ASSET,
+ TEST_SELECTED_ADDRESS,
TEST_CONTRACT_BALANCES,
false,
),
@@ -64,6 +69,7 @@ describe('Dapp Transactions utils :: validateTokenAmount', () => {
TEST_GAS,
TEST_INVALID_FROM as unknown as string,
TEST_SELECTED_ASSET,
+ TEST_SELECTED_ADDRESS,
TEST_CONTRACT_BALANCES,
false,
),
@@ -77,10 +83,49 @@ describe('Dapp Transactions utils :: validateTokenAmount', () => {
TEST_GAS,
TEST_FROM,
TEST_SELECTED_ASSET,
+ TEST_SELECTED_ADDRESS,
TEST_CONTRACT_BALANCES,
),
).toBeUndefined();
});
+
+ it('should check value from contractBalances if selectedAddress is from address', async () => {
+ const mockGetERC20BalanceOf = jest.fn().mockReturnValue('0x0');
+ Engine.context.AssetsContractController = {
+ getERC20BalanceOf: mockGetERC20BalanceOf,
+ };
+ expect(
+ await validateTokenAmount(
+ new BN(5),
+ TEST_GAS,
+ TEST_FROM,
+ TEST_SELECTED_ASSET,
+ TEST_FROM,
+ { '0x': '10' },
+ false,
+ ),
+ ).toEqual(undefined);
+ expect(mockGetERC20BalanceOf).toBeCalledTimes(0);
+ });
+
+ it('should call AssetsContractController.getERC20BalanceOf to get user balance if selectedAddress is not from address', async () => {
+ const mockGetERC20BalanceOf = jest.fn().mockReturnValue('0x0');
+ Engine.context.AssetsContractController = {
+ getERC20BalanceOf: mockGetERC20BalanceOf,
+ };
+
+ const result = await validateTokenAmount(
+ new BN(5),
+ TEST_GAS,
+ TEST_FROM,
+ TEST_SELECTED_ASSET,
+ TEST_SELECTED_ADDRESS,
+ { '0x': '10' },
+ false,
+ );
+ expect(mockGetERC20BalanceOf).toBeCalledTimes(1);
+ expect(result).toEqual(strings('transaction.insufficient'));
+ });
});
describe('Dapp Transactions utils :: validateCollectibleOwnership', () => {
diff --git a/app/util/dappTransactions/index.ts b/app/util/dappTransactions/index.ts
index da1edc43fd4..88309e92316 100644
--- a/app/util/dappTransactions/index.ts
+++ b/app/util/dappTransactions/index.ts
@@ -123,6 +123,7 @@ export const validateTokenAmount = async (
gas: BN,
from: string,
selectedAsset: SelectedAsset,
+ selectedAddress: string,
contractBalances: ContractBalances,
allowEmpty = true,
): Promise => {
@@ -143,13 +144,13 @@ export const validateTokenAmount = async (
// If user trying to send a token that doesn't own, validate balance querying contract
// If it fails, skip validation
let contractBalanceForAddress;
- if (contractBalances[selectedAsset.address]) {
+ if (selectedAddress === from && contractBalances[selectedAsset.address]) {
contractBalanceForAddress = hexToBN(
contractBalances[selectedAsset.address].toString(),
);
} else {
- const { AssetsContractController }: any = Engine.context;
try {
+ const { AssetsContractController }: any = Engine.context;
contractBalanceForAddress =
await AssetsContractController.getERC20BalanceOf(
selectedAsset.address,
@@ -216,6 +217,7 @@ export const validateAmount = async (
gas,
from,
selectedAsset,
+ selectedAddress,
contractBalances,
allowEmpty,
),
diff --git a/app/util/etherscan.js b/app/util/etherscan.js
index db47655a2e2..3a89d4d4d5f 100644
--- a/app/util/etherscan.js
+++ b/app/util/etherscan.js
@@ -1,4 +1,8 @@
-import { MAINNET } from '../constants/network';
+import {
+ LINEA_GOERLI_BLOCK_EXPLORER,
+ LINEA_MAINNET_BLOCK_EXPLORER,
+} from '../constants/urls';
+import { LINEA_GOERLI, LINEA_MAINNET, MAINNET } from '../constants/network';
/**
* Gets the etherscan link for an address in a specific network
@@ -29,6 +33,8 @@ export function getEtherscanTransactionUrl(network, tx_hash) {
* @returns - string
*/
export function getEtherscanBaseUrl(network) {
+ if (network === LINEA_GOERLI) return LINEA_GOERLI_BLOCK_EXPLORER;
+ if (network === LINEA_MAINNET) return LINEA_MAINNET_BLOCK_EXPLORER;
const subdomain =
network.toLowerCase() === MAINNET ? '' : `${network.toLowerCase()}.`;
return `https://${subdomain}etherscan.io`;
diff --git a/app/util/formatNumber.ts b/app/util/formatNumber.ts
index 752a671d13a..eeb395a67bc 100644
--- a/app/util/formatNumber.ts
+++ b/app/util/formatNumber.ts
@@ -1,4 +1,6 @@
+import BigNumber from 'bignumber.js';
+
const formatNumber = (value: number | string) =>
- value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ new BigNumber(value).toFormat();
export default formatNumber;
diff --git a/app/util/networks/customNetworks.tsx b/app/util/networks/customNetworks.tsx
index 2038a9c82cb..1ae3fe70c3a 100644
--- a/app/util/networks/customNetworks.tsx
+++ b/app/util/networks/customNetworks.tsx
@@ -95,17 +95,6 @@ const PopularList = [
imageSource: require('../../images/palm.png'),
},
},
- {
- chainId: '59140',
- nickname: 'Linea Goerli Test Network',
- rpcUrl: `https://rpc.goerli.linea.build`,
- ticker: 'LineaETH',
- rpcPrefs: {
- blockExplorerUrl: 'https://explorer.goerli.linea.build',
- imageUrl: 'LINEA_TESTNET',
- imageSource: require('../../images/linea-logo-dark.png'),
- },
- },
];
export default PopularList;
diff --git a/app/util/networks/index.js b/app/util/networks/index.js
index 4f6b3ee7fa7..ffbc847fec5 100644
--- a/app/util/networks/index.js
+++ b/app/util/networks/index.js
@@ -11,6 +11,8 @@ import {
NETWORKS_CHAIN_ID,
SEPOLIA,
RPC,
+ LINEA_GOERLI,
+ LINEA_MAINNET,
} from '../../../app/constants/network';
import { NetworkSwitchErrorType } from '../../../app/constants/error';
import { query } from '@metamask/controller-utils';
@@ -26,7 +28,8 @@ export { handleNetworkSwitch };
const ethLogo = require('../../images/eth-logo-new.png');
const goerliLogo = require('../../images/goerli-logo-dark.png');
const sepoliaLogo = require('../../images/sepolia-logo-dark.png');
-const lineaLogo = require('../../images/linea-logo-dark.png');
+const lineaGoerliLogo = require('../../images/linea-testnet-logo.png');
+const lineaMainnetLogo = require('../../images/linea-mainnet-logo.png');
/* eslint-enable */
import PopularList from './customNetworks';
@@ -55,6 +58,16 @@ const NetworkList = {
networkType: 'mainnet',
imageSource: ethLogo,
},
+ [LINEA_MAINNET]: {
+ name: 'Linea Main Network',
+ shortName: 'Linea',
+ networkId: 59144,
+ chainId: 59144,
+ hexChainId: '0xe708',
+ color: '#121212',
+ networkType: 'linea-mainnet',
+ imageSource: lineaMainnetLogo,
+ },
[GOERLI]: {
name: 'Goerli Test Network',
shortName: 'Goerli',
@@ -75,6 +88,16 @@ const NetworkList = {
networkType: 'sepolia',
imageSource: sepoliaLogo,
},
+ [LINEA_GOERLI]: {
+ name: 'Linea Goerli Test Network',
+ shortName: 'Linea Goerli',
+ networkId: 59140,
+ chainId: 59140,
+ hexChainId: '0xe704',
+ color: '#61dfff',
+ networkType: 'linea-goerli',
+ imageSource: lineaGoerliLogo,
+ },
[RPC]: {
name: 'Private Network',
shortName: 'Private',
@@ -101,6 +124,8 @@ export const isDefaultMainnet = (networkType) => networkType === MAINNET;
export const isMainNet = (network) =>
isDefaultMainnet(network?.providerConfig?.type) || network === String(1);
+export const isLineaMainnet = (networkType) => networkType === LINEA_MAINNET;
+
export const getDecimalChainId = (chainId) => {
if (!chainId || typeof chainId !== 'string' || !chainId.startsWith('0x')) {
return chainId;
@@ -111,6 +136,9 @@ export const getDecimalChainId = (chainId) => {
export const isMainnetByChainId = (chainId) =>
getDecimalChainId(String(chainId)) === String(1);
+export const isLineaMainnetByChainId = (chainId) =>
+ getDecimalChainId(String(chainId)) === String(59144);
+
export const isMultiLayerFeeNetwork = (chainId) =>
chainId === NETWORKS_CHAIN_ID.OPTIMISM;
@@ -124,7 +152,11 @@ export const getNetworkName = (id) =>
* @returns - Image of test network or undefined.
*/
export const getTestNetImage = (networkType) => {
- if (networkType === GOERLI || networkType === SEPOLIA) {
+ if (
+ networkType === GOERLI ||
+ networkType === SEPOLIA ||
+ networkType === LINEA_GOERLI
+ ) {
return networksWithImages?.[networkType.toUpperCase()];
}
};
@@ -136,16 +168,19 @@ export const getTestNetImageByChainId = (chainId) => {
if (NETWORKS_CHAIN_ID.SEPOLIA === chainId) {
return networksWithImages?.SEPOLIA;
}
- if (NETWORKS_CHAIN_ID.LINEA_TESTNET === chainId) {
- return networksWithImages?.LINEA_TESTNET;
+ if (NETWORKS_CHAIN_ID.LINEA_GOERLI === chainId) {
+ return networksWithImages?.['LINEA-GOERLI'];
}
};
export const isTestNet = (networkId) => {
- if (networkId === NETWORKS_CHAIN_ID.LINEA_TESTNET) return true;
const networkName = getNetworkName(networkId);
- return networkName === GOERLI || networkName === SEPOLIA;
+ return (
+ networkName === GOERLI ||
+ networkName === SEPOLIA ||
+ networkName === LINEA_GOERLI
+ );
};
export function getNetworkTypeById(id) {
@@ -335,9 +370,16 @@ export const getNetworkNameFromProvider = (provider) => {
export const getNetworkImageSource = ({ networkType, chainId }) => {
const defaultNetwork = getDefaultNetworkByChainId(chainId);
const isDefaultEthMainnet = isDefaultMainnet(networkType);
+ const isLineaMainnetNetwork = isLineaMainnet(networkType);
+
if (defaultNetwork && isDefaultEthMainnet) {
return defaultNetwork.imageSource;
}
+
+ if (defaultNetwork && isLineaMainnetNetwork) {
+ return defaultNetwork.imageSource;
+ }
+
const popularNetwork = PopularList.find(
(network) => network.chainId === chainId,
);
@@ -437,3 +479,6 @@ export const getBlockExplorerTxUrl = (
*/
export const getIsNetworkOnboarded = (chainId, networkOnboardedState) =>
networkOnboardedState[chainId];
+
+export const shouldShowLineaMainnetNetwork = () =>
+ new Date().getTime() > Date.UTC(2023, 6, 11, 18);
diff --git a/app/util/networks/index.test.ts b/app/util/networks/index.test.ts
index 59115e84d38..231f7051d68 100644
--- a/app/util/networks/index.test.ts
+++ b/app/util/networks/index.test.ts
@@ -9,7 +9,14 @@ import {
getBlockExplorerAddressUrl,
getBlockExplorerTxUrl,
} from '.';
-import { MAINNET, GOERLI, RPC, SEPOLIA } from '../../../app/constants/network';
+import {
+ MAINNET,
+ GOERLI,
+ RPC,
+ SEPOLIA,
+ LINEA_GOERLI,
+ LINEA_MAINNET,
+} from '../../../app/constants/network';
import { NetworkSwitchErrorType } from '../../../app/constants/error';
import Engine from './../../core/Engine';
@@ -37,6 +44,8 @@ describe('NetworkUtils::getAllNetworks', () => {
expect(allNetworks.includes(MAINNET)).toEqual(true);
expect(allNetworks.includes(SEPOLIA)).toEqual(true);
expect(allNetworks.includes(GOERLI)).toEqual(true);
+ expect(allNetworks.includes(LINEA_GOERLI)).toEqual(true);
+ expect(allNetworks.includes(LINEA_MAINNET)).toEqual(true);
});
it('should exclude rpc', () => {
expect(allNetworks.includes(RPC)).toEqual(false);
@@ -83,6 +92,17 @@ describe('NetworkUtils::getNetworkName', () => {
const main = getNetworkName(String(11155111));
expect(main).toEqual(SEPOLIA);
});
+
+ it(`should get network name for ${LINEA_GOERLI} id`, () => {
+ const main = getNetworkName(String(59140));
+ expect(main).toEqual(LINEA_GOERLI);
+ });
+
+ it(`should get network name for ${LINEA_MAINNET} id`, () => {
+ const main = getNetworkName(String(59144));
+ expect(main).toEqual(LINEA_MAINNET);
+ });
+
it(`should return undefined for unknown network id`, () => {
const main = getNetworkName(String(99));
expect(main).toEqual(undefined);
@@ -215,7 +235,7 @@ describe('NetworkUtils::handleNetworkSwitch', () => {
describe('NetworkUtils::getBlockExplorerAddressUrl', () => {
const mockEthereumAddress = '0x0000000000000000000000000000000000000001';
- it('should return null result when network type === "rpc" | network type === "lineatestnet" and rpcBlockExplorerUrl === null', () => {
+ it('should return null result when network type === "rpc" and rpcBlockExplorerUrl === null', () => {
const { url, title } = getBlockExplorerAddressUrl(RPC, mockEthereumAddress);
expect(url).toBe(null);
@@ -233,7 +253,7 @@ describe('NetworkUtils::getBlockExplorerAddressUrl', () => {
expect(title).toBe(`avalanche-rpc-url`);
});
- it('should return etherscan block explorer address url when network type !== "rpc" and type !== "lineatestnet"', () => {
+ it('should return etherscan block explorer address url when network type !== "rpc"', () => {
const { url, title } = getBlockExplorerAddressUrl(
GOERLI,
mockEthereumAddress,
@@ -244,6 +264,28 @@ describe('NetworkUtils::getBlockExplorerAddressUrl', () => {
);
expect(title).toBe(`goerli.etherscan.io`);
});
+
+ it('should return custom block explorer address url when network type === "linea-goerli"', () => {
+ const { url, title } = getBlockExplorerAddressUrl(
+ LINEA_GOERLI,
+ mockEthereumAddress,
+ );
+
+ expect(url).toBe(
+ `https://goerli.lineascan.build/address/${mockEthereumAddress}`,
+ );
+ expect(title).toBe(`goerli.lineascan.build`);
+ });
+
+ it('should return custom block explorer address url when network type === "linea-mainnet"', () => {
+ const { url, title } = getBlockExplorerAddressUrl(
+ LINEA_MAINNET,
+ mockEthereumAddress,
+ );
+
+ expect(url).toBe(`https://lineascan.build/address/${mockEthereumAddress}`);
+ expect(title).toBe(`lineascan.build`);
+ });
});
describe('NetworkUtils::getBlockExplorerTxUrl', () => {
@@ -257,7 +299,7 @@ describe('NetworkUtils::getBlockExplorerTxUrl', () => {
expect(title).toBe(null);
});
- it('should return rpc block explorer address url when network type === "rpc"', () => {
+ it('should return rpc block explorer tx url when network type === "rpc"', () => {
const { url, title } = getBlockExplorerTxUrl(
RPC,
mockTransactionHash,
@@ -268,10 +310,32 @@ describe('NetworkUtils::getBlockExplorerTxUrl', () => {
expect(title).toBe(`avalanche-rpc-url`);
});
- it('should return etherscan block explorer address url when network type !== "rpc" and type !== "lineatestnet"', () => {
+ it('should return etherscan block explorer tx url when network type !== "rpc"', () => {
const { url, title } = getBlockExplorerTxUrl(GOERLI, mockTransactionHash);
expect(url).toBe(`https://goerli.etherscan.io/tx/${mockTransactionHash}`);
expect(title).toBe(`goerli.etherscan.io`);
});
+
+ it('should return custom block explorer tx url when network type === "linea-goerli"', () => {
+ const { url, title } = getBlockExplorerTxUrl(
+ LINEA_GOERLI,
+ mockTransactionHash,
+ );
+
+ expect(url).toBe(
+ `https://goerli.lineascan.build/tx/${mockTransactionHash}`,
+ );
+ expect(title).toBe(`goerli.lineascan.build`);
+ });
+
+ it('should return custom block explorer tx url when network type === "linea-mainnet"', () => {
+ const { url, title } = getBlockExplorerTxUrl(
+ LINEA_MAINNET,
+ mockTransactionHash,
+ );
+
+ expect(url).toBe(`https://lineascan.build/tx/${mockTransactionHash}`);
+ expect(title).toBe(`lineascan.build`);
+ });
});
diff --git a/app/util/transactions/index.js b/app/util/transactions/index.js
index e7adafe608d..63bdced89ae 100644
--- a/app/util/transactions/index.js
+++ b/app/util/transactions/index.js
@@ -3,8 +3,7 @@ import { rawEncode, rawDecode } from 'ethereumjs-abi';
import BigNumber from 'bignumber.js';
import humanizeDuration from 'humanize-duration';
import { query, isSmartContractCode } from '@metamask/controller-utils';
-// TODO: Update after this function has been exported from the package
-import { isEIP1559Transaction } from '@metamask/transaction-controller/dist/utils';
+import { isEIP1559Transaction } from '@metamask/transaction-controller';
import { swapsUtils } from '@metamask/swaps-controller';
import Engine from '../../core/Engine';
import I18n, { strings } from '../../../locales/i18n';
@@ -721,6 +720,20 @@ export const calculateEIP1559Times = ({
hasTime = true;
}
+ if (
+ Number(suggestedMaxPriorityFeePerGas) >=
+ Number(gasFeeEstimates[HIGH].suggestedMaxPriorityFeePerGas)
+ ) {
+ timeEstimate = `${strings(
+ 'times_eip1559.likely_in',
+ )} ${humanizeDuration(
+ gasFeeEstimates[HIGH].minWaitTimeEstimate,
+ timeParams,
+ )}`;
+ timeEstimateColor = 'orange';
+ timeEstimateId = AppConstants.GAS_TIMES.VERY_LIKELY;
+ }
+
if (hasTime) {
return { timeEstimate, timeEstimateColor, timeEstimateId };
}
diff --git a/app/util/transactions/index.test.ts b/app/util/transactions/index.test.ts
index 7994cf55fb5..971f6710488 100644
--- a/app/util/transactions/index.test.ts
+++ b/app/util/transactions/index.test.ts
@@ -18,6 +18,7 @@ import {
TOKEN_METHOD_TRANSFER,
CONTRACT_METHOD_DEPLOY,
TOKEN_METHOD_TRANSFER_FROM,
+ calculateEIP1559Times,
} from '.';
import { buildUnserializedTransaction } from './optimismTransaction';
import Engine from '../../core/Engine';
@@ -387,6 +388,96 @@ describe('Transactions utils :: minimumTokenAllowance', () => {
});
});
+describe('Transaction utils :: calculateEIP1559Times', () => {
+ const gasFeeEstimates = {
+ baseFeeTrend: 'down',
+ estimatedBaseFee: '2.420440144',
+ high: {
+ maxWaitTimeEstimate: 60000,
+ minWaitTimeEstimate: 15000,
+ suggestedMaxFeePerGas: '6.114748245',
+ suggestedMaxPriorityFeePerGas: '2',
+ },
+ historicalBaseFeeRange: ['2.420440144', '9.121942855'],
+ historicalPriorityFeeRange: ['0.006333568', '2997.107725'],
+ latestPriorityFeeRange: ['0.039979856', '5'],
+ low: {
+ maxWaitTimeEstimate: 30000,
+ minWaitTimeEstimate: 15000,
+ suggestedMaxFeePerGas: '3.420440144',
+ suggestedMaxPriorityFeePerGas: '1',
+ },
+ medium: {
+ maxWaitTimeEstimate: 45000,
+ minWaitTimeEstimate: 15000,
+ suggestedMaxFeePerGas: '4.767594195',
+ suggestedMaxPriorityFeePerGas: '1.5',
+ },
+ networkCongestion: 0,
+ priorityFeeTrend: 'level',
+ };
+
+ it('returns data for very large gas fees estimates', () => {
+ const EIP1559Times = calculateEIP1559Times({
+ suggestedMaxFeePerGas: 1000000,
+ suggestedMaxPriorityFeePerGas: 1000000,
+ gasFeeEstimates,
+ selectedOption: 'medium',
+ recommended: undefined,
+ });
+ expect(EIP1559Times).toStrictEqual({
+ timeEstimate: 'Likely in 15 seconds',
+ timeEstimateColor: 'orange',
+ timeEstimateId: 'very_likely',
+ });
+ });
+
+ it('returns data for aggresive gas fees estimates', () => {
+ const EIP1559Times = calculateEIP1559Times({
+ suggestedMaxFeePerGas: 5.320770797,
+ suggestedMaxPriorityFeePerGas: 2,
+ gasFeeEstimates,
+ selectedOption: 'high',
+ recommended: undefined,
+ });
+ expect(EIP1559Times).toStrictEqual({
+ timeEstimate: 'Likely in 15 seconds',
+ timeEstimateColor: 'orange',
+ timeEstimateId: 'very_likely',
+ });
+ });
+
+ it('returns data for market gas fees estimates', () => {
+ const EIP1559Times = calculateEIP1559Times({
+ suggestedMaxFeePerGas: 4.310899437,
+ suggestedMaxPriorityFeePerGas: 1.5,
+ gasFeeEstimates,
+ selectedOption: 'medium',
+ recommended: undefined,
+ });
+ expect(EIP1559Times).toStrictEqual({
+ timeEstimate: 'Likely in < 30 seconds',
+ timeEstimateColor: 'green',
+ timeEstimateId: 'likely',
+ });
+ });
+
+ it('returns data for low gas fees estimates', () => {
+ const EIP1559Times = calculateEIP1559Times({
+ suggestedMaxFeePerGas: 2.667821471,
+ suggestedMaxPriorityFeePerGas: 1,
+ gasFeeEstimates,
+ selectedOption: 'low',
+ recommended: undefined,
+ });
+ expect(EIP1559Times).toStrictEqual({
+ timeEstimate: 'Maybe in 30 seconds',
+ timeEstimateColor: 'red',
+ timeEstimateId: 'maybe',
+ });
+ });
+});
+
describe('Transactions utils :: buildUnserializedTransaction', () => {
it('returns a transaction that can be serialized and fed to an Optimism smart contract', () => {
const unserializedTransaction = buildUnserializedTransaction({
diff --git a/bitrise.yml b/bitrise.yml
index 327f13367d8..ed37dceb586 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -21,24 +21,38 @@ pipelines:
- create_build_release: {}
- deploy_build_release: {}
- release_notify: {}
+ #Releases MetaMask apps and stores ipa into App(TestFlight) Store
+ release_ios_to_store_pipeline:
+ stages:
+ - create_ios_release: {}
+ - deploy_ios_release: {}
+ - notify: {}
+ #Releases MetaMask apps and stores apk Play(Internal Testing) Store
+ release_android_to_store_pipeline:
+ stages:
+ - create_android_release: {}
+ - deploy_android_release: {}
+ - notify: {}
#Run E2E test suite for iOS only
run_e2e_ios_pipeline:
stages:
- run_e2e_ios_stage: {}
+ - notify: {}
#Run E2E test suite for Android only
run_e2e_android_pipeline:
stages:
- - run_e2e_android_stage: {}
+ - create_build_qa_android: {} #builds app and kicks off separate E2E build
+ - notify: {}
#PR_e2e_verfication (build ios & android), run iOS (smoke), emulator Android
release_e2e_pipeline:
stages:
- - run_e2e_ios_stage: {}
- - run_e2e_android_stage: {}
+ - run_e2e_ios_android_stage: {}
+ - notify: {}
#PR_e2e_verfication (build ios & android), run iOS (smoke), emulator Android
pr_smoke_e2e_pipeline:
stages:
- - run_e2e_ios_stage: {}
- # - run_e2e_android_stage: {}
+ - run_smoke_e2e_ios_android_stage: {}
+ - notify: {}
#Stages refrence workflows. Those workflows cannot but utility "_this-is-a-utility"
stages:
@@ -50,17 +64,42 @@ stages:
workflows:
- deploy_android_to_store: {}
- deploy_ios_to_store: {}
+ create_ios_release:
+ workflows:
+ - build_ios_release: {}
+ deploy_ios_release:
+ workflows:
+ - deploy_ios_to_store: {}
+ create_android_release:
+ workflows:
+ - build_android_release: {}
+ deploy_android_release:
+ workflows:
+ - deploy_android_to_store: {}
create_build_qa:
workflows:
- build_android_qa: {}
- build_ios_qa: {}
+ create_build_qa_android:
+ workflows:
+ - build_android_qa: {}
+ create_build_qa_ios:
+ workflows:
+ - build_ios_qa: {}
run_e2e_ios_stage:
workflows:
- ios_e2e_test: {}
+ run_smoke_e2e_ios_android_stage:
+ workflows:
+ - ios_e2e_test: {}
+ - build_android_qa: {}
+ run_e2e_ios_android_stage:
+ workflows:
+ - ios_e2e_test: {}
+ - build_android_qa: {}
run_e2e_android_stage:
workflows:
- - build_android_release: {}
- - wdio_android_e2e_test: {} #Build QA -> Browserstack -> wdio_ (can be a smaller machine)
+ - build_android_qa: {} #Build QA -> Browserstack -> wdio_ (can be a smaller machine)
notify:
workflows:
- notify_success_on_slack: {}
@@ -81,7 +120,13 @@ workflows:
before_run:
- setup
steps:
- - restore-npm-cache@1: {}
+ - script@1:
+ inputs:
+ - content: |-
+ #!/usr/bin/env bash
+ envman add --key YARN_CACHE_DIR --value "$(yarn cache dir)"
+ title: Get Yarn cache directory
+ - cache-pull@2: {}
- yarn@0:
inputs:
- command: setup
@@ -94,7 +139,12 @@ workflows:
inputs:
- command: audit:ci
title: Audit Dependencies
- - save-npm-cache@1: {}
+ - cache-push@2:
+ # Manual cache path needed here because the `yarn` step only caches for the `install` command
+ inputs:
+ # The Yarn cache is easier and safer to cache than `node_modules`, which could include
+ # postinstall script modifications and could affect hoisting on subsequent installs.
+ - cache_paths: "$YARN_CACHE_DIR -> ./yarn.lock"
code_setup_dev:
before_run:
- setup
@@ -108,6 +158,38 @@ workflows:
after_run:
- code_setup
+ # Notifications utility workflows
+ # Provides values for commit or branch message and path depending on commit env setup initialised or not
+ _get_workflow_info:
+ steps:
+ - activate-ssh-key@4:
+ is_always_run: true # always run to also feed failure notifications
+ run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
+ - git-clone@6:
+ inputs:
+ - update_submodules: 'no'
+ is_always_run: true # always run to also feed failure notifications
+ - script@1:
+ is_always_run: true # always run to also feed failure notifications
+ inputs:
+ - content: |
+ #!/bin/bash
+ # generate reference to commit from env or using git
+ COMMIT_SHORT_HASH="${BITRISE_GIT_COMMIT:0:7}"
+ BRANCH_HEIGHT=''
+ WORKFLOW_TRIGGER='Push'
+
+ if [[ -z "$BITRISE_GIT_COMMIT" ]]; then
+ COMMIT_SHORT_HASH="$(git rev-parse --short HEAD)"
+ BRANCH_HEIGHT='HEAD'
+ WORKFLOW_TRIGGER='Manual'
+ fi
+
+ envman add --key COMMIT_SHORT_HASH --value "$COMMIT_SHORT_HASH"
+ envman add --key BRANCH_HEIGHT --value "$BRANCH_HEIGHT"
+ envman add --key WORKFLOW_TRIGGER --value "$WORKFLOW_TRIGGER"
+ title: Get commit or branch name and path variables
+
# Slack notification utils: we have two workflows to allow choosing when to notify: on success, on failure or both.
# A workflow for instance create_qa_builds will notify on failure for each build_android_qa or build_ios_qa
# but will only notify success if both success and create_qa_builds succeeds.
@@ -129,6 +211,8 @@ workflows:
# Send a Slack message when workflow succeeds
notify_success_on_slack:
+ before_run:
+ - _get_workflow_info
steps:
- slack@3:
inputs:
@@ -151,6 +235,8 @@ workflows:
# Send a Slack message when workflow fails
_notify_failure_on_slack:
+ before_run:
+ - _get_workflow_info
steps:
- slack@3:
is_always_run: true
@@ -481,8 +567,6 @@ workflows:
stack: linux-docker-android-20.04
machine_type_id: standard
deploy_android_to_store:
- # before_run:
- # - build_android_release
steps:
- pull-intermediate-files@1:
inputs:
@@ -498,8 +582,6 @@ workflows:
is_expand: false
MM_ANDROID_PACKAGE_NAME: io.metamask
deploy_ios_to_store:
- # before_run:
- # - build_ios_release
steps:
- pull-intermediate-files@1:
inputs:
@@ -519,7 +601,6 @@ workflows:
- build_short_version_string: $VERSION_NAME
- build_version: $VERSION_NUMBER
- plist_path: $PROJECT_LOCATION_IOS/MetaMask/Info.plist
- - restore-cocoapods-cache@1: {}
- cocoapods-install@2: {}
- script@1:
inputs:
@@ -529,7 +610,6 @@ workflows:
METAMASK_ENVIRONMENT='production' yarn build:ios:pre-release
title: iOS Sourcemaps & Build
is_always_run: false
- - save-cocoapods-cache@1: {}
- deploy-to-bitrise-io@2.2.3:
is_always_run: false
is_skippable: true
@@ -562,7 +642,6 @@ workflows:
- build_short_version_string: $VERSION_NAME
- build_version: $VERSION_NUMBER
- plist_path: $PROJECT_LOCATION_IOS/MetaMask/MetaMask-QA-Info.plist
- - restore-cocoapods-cache@1: {}
- cocoapods-install@2: {}
- script@1:
inputs:
@@ -572,7 +651,6 @@ workflows:
GIT_BRANCH=$BITRISE_GIT_BRANCH METAMASK_ENVIRONMENT='qa' yarn build:ios:pre-qa
title: iOS Sourcemaps & Build
is_always_run: false
- - save-cocoapods-cache@1: {}
- deploy-to-bitrise-io@2.2.3:
is_always_run: false
is_skippable: true
@@ -644,9 +722,9 @@ meta:
trigger_map:
- push_branch: release/*
pipeline: release_e2e_pipeline
- - tag: "v*.*.*-RC-*"
+ - tag: 'v*.*.*-RC-*'
pipeline: release_builds_to_store_pipeline
- - tag: "qa-*"
+ - tag: 'qa-*'
pipeline: create_qa_builds_pipeline
- - tag: "dev-e2e-*"
+ - tag: 'dev-e2e-*'
pipeline: pr_smoke_e2e_pipeline
diff --git a/e2e/helpers.js b/e2e/helpers.js
index 7118d5b7303..bf5e838d482 100644
--- a/e2e/helpers.js
+++ b/e2e/helpers.js
@@ -41,7 +41,7 @@ export default class TestHelpers {
}
static tapItemAtIndex(elementID, index) {
- return element(by.id(elementID, index))
+ return element(by.id(elementID))
.atIndex(index || 0)
.tap();
}
@@ -92,8 +92,14 @@ export default class TestHelpers {
return element(by.label(text)).atIndex(0).tap();
}
- static async swipe(elementId, direction, speed, percentage) {
- await element(by.id(elementId)).swipe(direction, speed, percentage);
+ static async swipe(elementId, direction, speed, percentage, xStart, yStart) {
+ await element(by.id(elementId)).swipe(
+ direction,
+ speed,
+ percentage,
+ xStart,
+ yStart,
+ );
}
static async swipeByText(text, direction, speed, percentage) {
@@ -172,4 +178,23 @@ export default class TestHelpers {
}, ms);
});
}
+
+ static async retry(maxAttempts, testLogic) {
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ await testLogic();
+ return;
+ } catch (error) {
+ if (attempt === maxAttempts) {
+ throw error;
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('Test attempt failed', {
+ attempt,
+ error,
+ });
+ }
+ }
+ }
+ }
}
diff --git a/e2e/pages/AccountListView.js b/e2e/pages/AccountListView.js
index a692371b120..21f40cdfb54 100644
--- a/e2e/pages/AccountListView.js
+++ b/e2e/pages/AccountListView.js
@@ -1,39 +1,28 @@
import TestHelpers from '../helpers';
+import { ACCOUNT_LIST_ID } from '../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds';
import {
- ACCOUNT_LIST_ID,
- CREATE_ACCOUNT_BUTTON_ID,
- IMPORT_ACCOUNT_BUTTON_ID,
-} from '../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds';
-import { CELL_SELECT_TEST_ID } from '../../app/constants/test-ids';
+ CELL_MULTI_SELECT_TEST_ID,
+ CELL_SELECT_TEST_ID,
+} from '../../app/constants/test-ids';
import messages from '../../locales/languages/en.json';
const REMOVE_IMPORTED_ACCOUNT_TEXT = messages.accounts.yes_remove_it;
export default class AccountListView {
- static async tapCreateAccountButton() {
- await TestHelpers.waitAndTap(CREATE_ACCOUNT_BUTTON_ID);
- }
-
- static async tapImportAccountButton() {
- await TestHelpers.waitAndTap(IMPORT_ACCOUNT_BUTTON_ID);
- }
-
- static async tapAccountByName(accountName) {
- await TestHelpers.tapByText(accountName);
+ static async tapNewAccount2() {
+ await TestHelpers.tapItemAtIndex(CELL_MULTI_SELECT_TEST_ID, 1);
}
static async longPressImportedAccount() {
await TestHelpers.tapAndLongPressAtIndex(CELL_SELECT_TEST_ID, 1);
}
- static async swipeOnAccounts() {
- await TestHelpers.swipe(ACCOUNT_LIST_ID, 'down', 'slow', 0.6);
- }
static async swipeToDimssAccountsModal() {
await TestHelpers.swipeByText('Accounts', 'down', 'slow', 0.6);
}
+
static async tapYesToRemoveImportedAccountAlertButton() {
await TestHelpers.tapAlertWithButton(REMOVE_IMPORTED_ACCOUNT_TEXT);
}
@@ -52,9 +41,6 @@ export default class AccountListView {
}
}
- static async isAccountNameVisible() {
- await TestHelpers.checkIfElementWithTextIsVisible('Account 2');
- }
static async accountNameNotVisible() {
await TestHelpers.checkIfElementWithTextIsNotVisible('Account 2');
}
diff --git a/e2e/pages/Drawer/Browser.js b/e2e/pages/Drawer/Browser.js
index b9f6314a838..7205f4fcc51 100644
--- a/e2e/pages/Drawer/Browser.js
+++ b/e2e/pages/Drawer/Browser.js
@@ -17,6 +17,7 @@ import {
ADD_BOOKMARKS_BUTTON_ID,
} from '../../../wdio/screen-objects/testIDs/BrowserScreen/AddFavorite.testIds';
import { NOTIFICATION_TITLE } from '../../../wdio/screen-objects/testIDs/Components/Notification.testIds';
+import { TEST_DAPP_URL } from '../TestDApp';
const ANDROID_BROWSER_WEBVIEW_ID = 'browser-webview';
const ANDROID_CLEAR_INPUT_BUTTON_ID = 'cancel-url-button';
@@ -135,4 +136,10 @@ export default class Browser {
REVOKE_ALL_ACCOUNTS_TEXT,
);
}
+
+ static async navigateToTestDApp() {
+ await Browser.tapUrlInputBox();
+ await Browser.navigateToURL(TEST_DAPP_URL);
+ await TestHelpers.delay(3000);
+ }
}
diff --git a/e2e/pages/Drawer/DrawerView.js b/e2e/pages/Drawer/DrawerView.js
deleted file mode 100644
index a95783deb03..00000000000
--- a/e2e/pages/Drawer/DrawerView.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import TestHelpers from '../../helpers';
-
-const ACCOUNT_CARET_BUTTON_ID = 'navbar-account-button';
-const ADD_FUNDS_BUTTON_ID = 'drawer-receive-button';
-const DRAWER_CONTAINER_ID = 'drawer-screen';
-const SEND_BUTTON_ID = 'drawer-send-button';
-export default class DrawerView {
- static async tapSettings() {
- await TestHelpers.tapByText('Settings');
- }
-
- static async tapTransactions() {
- await TestHelpers.tapByText('Transactions');
- }
- static async tapLockAccount() {
- await TestHelpers.tapByText('Lock');
- }
-
- static async tapYesAlertButton() {
- await TestHelpers.tapAlertWithButton('YES'); // Do you really want to log out modal
- }
-
- static async tapAccountCaretButton() {
- await TestHelpers.waitAndTap(ACCOUNT_CARET_BUTTON_ID);
- }
- static async closeDrawer() {
- if (device.getPlatform() === 'android') {
- await device.pressBack();
- await TestHelpers.delay(1000);
- } else {
- // Close drawer
- await TestHelpers.swipe(DRAWER_CONTAINER_ID, 'left');
- }
- }
-
- static async tapSendButton() {
- await TestHelpers.tap(SEND_BUTTON_ID);
- }
-
- static async tapOnAddFundsButton() {
- await TestHelpers.tap(ADD_FUNDS_BUTTON_ID);
- }
-
- static async isVisible() {
- await TestHelpers.checkIfVisible(DRAWER_CONTAINER_ID);
- }
-
- static async isNotVisible() {
- await TestHelpers.checkIfNotVisible(DRAWER_CONTAINER_ID);
- }
-}
diff --git a/e2e/pages/Drawer/Settings/AdvancedView.js b/e2e/pages/Drawer/Settings/AdvancedView.js
new file mode 100644
index 00000000000..a29e1b8a61d
--- /dev/null
+++ b/e2e/pages/Drawer/Settings/AdvancedView.js
@@ -0,0 +1,12 @@
+import {
+ ADVANCED_SETTINGS_CONTAINER_ID,
+ ETH_SIGN_SWITCH_ID,
+} from '../../../../app/constants/test-ids';
+import TestHelpers from '../../../helpers';
+
+export default class AdvancedSettingsView {
+ static async tapEthSignSwitch() {
+ await TestHelpers.swipe(ADVANCED_SETTINGS_CONTAINER_ID, 'up', 'slow', 0.2);
+ await TestHelpers.tap(ETH_SIGN_SWITCH_ID);
+ }
+}
diff --git a/e2e/pages/Drawer/Settings/NetworksView.js b/e2e/pages/Drawer/Settings/NetworksView.js
index 662b7bc3849..27aa40dad43 100644
--- a/e2e/pages/Drawer/Settings/NetworksView.js
+++ b/e2e/pages/Drawer/Settings/NetworksView.js
@@ -4,7 +4,7 @@ import {
ADD_CUSTOM_RPC_NETWORK_BUTTON_ID,
ADD_NETWORKS_ID,
} from '../../../../app/constants/test-ids';
-import NetworkEducationModal from '../../modals/NetworkEducationModal';
+import { NETWORK_BACK_ARROW_BUTTON_ID } from '../../../../wdio/screen-objects/testIDs/Screens/NetworksScreen.testids';
const NETWORK_VIEW_CONTAINER_ID = 'networks-screen';
const RPC_NETWORK_NAME_ID = 'rpc-networks';
@@ -17,7 +17,7 @@ const RPC_WARNING_BANNER_ID = 'rpc-url-warning';
export default class NetworkView {
static async tapAddNetworkButton() {
- await TestHelpers.tap(ADD_NETWORKS_ID);
+ await TestHelpers.waitAndTap(ADD_NETWORKS_ID);
}
static async switchToCustomNetworks() {
@@ -62,7 +62,7 @@ export default class NetworkView {
}
static async tapRpcNetworkAddButton() {
- await TestHelpers.tap(ADD_CUSTOM_RPC_NETWORK_BUTTON_ID);
+ await TestHelpers.waitAndTap(ADD_CUSTOM_RPC_NETWORK_BUTTON_ID);
}
static async swipeToRPCTitleAndDismissKeyboard() {
@@ -83,9 +83,7 @@ export default class NetworkView {
// Go back to wallet screen
if (device.getPlatform() === 'ios') {
// Tap on back arrow
- await TestHelpers.waitAndTap('nav-ios-back');
- // Tap close
- await NetworkEducationModal.tapGotItButton();
+ await TestHelpers.waitAndTap(NETWORK_BACK_ARROW_BUTTON_ID);
} else {
// Go Back for android
await TestHelpers.waitAndTap('nav-android-back');
diff --git a/e2e/pages/Drawer/Settings/SettingsView.js b/e2e/pages/Drawer/Settings/SettingsView.js
index 6665d5bb563..7ff2aec3fff 100644
--- a/e2e/pages/Drawer/Settings/SettingsView.js
+++ b/e2e/pages/Drawer/Settings/SettingsView.js
@@ -1,21 +1,41 @@
import TestHelpers from '../../../helpers';
+import {
+ CONTACTS_SETTINGS,
+ GENERAL_SETTINGS,
+ LOCK_SETTINGS,
+ NETWORKS_SETTINGS,
+ SECURITY_SETTINGS,
+} from '../../../../wdio/screen-objects/testIDs/Screens/Settings.testIds';
const ANDROID_BACK_BUTTON_ON_SETTINGS_PAGE_ID = 'nav-android-back';
export default class SettingsView {
static async tapGeneral() {
- await TestHelpers.tapByText('General');
+ await TestHelpers.waitAndTap(GENERAL_SETTINGS);
+ }
+
+ static async tapAdvanced() {
+ await TestHelpers.tapByText('Advanced');
}
static async tapContacts() {
- await TestHelpers.tapByText('Contacts');
+ await TestHelpers.waitAndTap(CONTACTS_SETTINGS);
}
static async tapSecurityAndPrivacy() {
- await TestHelpers.tapByText('Security & Privacy');
+ await TestHelpers.waitAndTap(SECURITY_SETTINGS);
}
static async tapNetworks() {
- await TestHelpers.tapByText('Networks');
+ await TestHelpers.waitAndTap(NETWORKS_SETTINGS);
+ }
+
+ static async tapLock() {
+ await TestHelpers.swipe(LOCK_SETTINGS, 'up', 'fast');
+ await TestHelpers.waitAndTap(LOCK_SETTINGS);
+ }
+
+ static async tapYesAlertButton() {
+ await TestHelpers.tapAlertWithButton('YES'); // Do you really want to log out modal
}
static async tapCloseButton() {
diff --git a/e2e/pages/SendView.js b/e2e/pages/SendView.js
index c696addfddd..aa6175efd0b 100644
--- a/e2e/pages/SendView.js
+++ b/e2e/pages/SendView.js
@@ -1,31 +1,35 @@
import TestHelpers from '../helpers';
-import { ADDRESS_BOOK_NEXT_BUTTON } from '../../app/constants/test-ids';
+import {
+ ADDRESS_BOOK_NEXT_BUTTON,
+ ADDRESS_ERROR,
+ NO_ETH_MESSAGE,
+} from '../../app/constants/test-ids';
+import {
+ ADD_ADDRESS_BUTTON,
+ SEND_ADDRESS_INPUT_FIELD,
+ SEND_CANCEL_BUTTON,
+} from '../../wdio/screen-objects/testIDs/Screens/SendScreen.testIds';
-const ADDRESS_INPUT_BOX_ID = 'txn-to-address-input';
-const ADD_TO_ADDRESS_BOOK_BUTTON_ID = 'add-address-button';
-const CANCEL_BUTTON_ID = 'send-cancel-button';
-const INCORRECT_ADDRESS_ERROR_ID = 'address-error';
-const NO_ETH_WARNING_MESSAGE_ID = 'no-eth-message';
const MY_ACCOUNTS_BUTTON_ID = 'my-accounts-button';
const REMOVE_ADDRESS_BUTTON_ID = 'clear-address-button';
export default class SendView {
- static async tapcancelButton() {
- await TestHelpers.tap(CANCEL_BUTTON_ID);
+ static async tapCancelButton() {
+ await TestHelpers.tap(SEND_CANCEL_BUTTON);
}
static async tapNextButton() {
await TestHelpers.waitAndTap(ADDRESS_BOOK_NEXT_BUTTON);
}
static async inputAddress(address) {
- await TestHelpers.replaceTextInField(ADDRESS_INPUT_BOX_ID, address);
+ await TestHelpers.replaceTextInField(SEND_ADDRESS_INPUT_FIELD, address);
}
static async tapAndLongPress() {
- await TestHelpers.tapAndLongPress(ADDRESS_INPUT_BOX_ID);
+ await TestHelpers.tapAndLongPress(SEND_ADDRESS_INPUT_FIELD);
}
static async tapAddAddressToAddressBook() {
- await TestHelpers.waitAndTap(ADD_TO_ADDRESS_BOOK_BUTTON_ID);
+ await TestHelpers.waitAndTap(ADD_ADDRESS_BUTTON);
}
static async removeAddress() {
@@ -41,12 +45,12 @@ export default class SendView {
}
static async incorrectAddressErrorMessageIsVisible() {
- await TestHelpers.checkIfVisible(INCORRECT_ADDRESS_ERROR_ID);
+ await TestHelpers.checkIfVisible(ADDRESS_ERROR);
}
static async noEthWarningMessageIsVisible() {
//Check that the warning appears at the bottom of the screen
- await TestHelpers.checkIfVisible(NO_ETH_WARNING_MESSAGE_ID);
+ await TestHelpers.checkIfVisible(NO_ETH_MESSAGE);
}
static async isSavedAliasVisible(name) {
diff --git a/e2e/pages/TabBarComponent.js b/e2e/pages/TabBarComponent.js
index 9d9a59b774c..31e612bb940 100644
--- a/e2e/pages/TabBarComponent.js
+++ b/e2e/pages/TabBarComponent.js
@@ -2,6 +2,7 @@ import TestHelpers from '../helpers';
import {
TAB_BAR_ACTION_BUTTON,
TAB_BAR_BROWSER_BUTTON,
+ TAB_BAR_SETTING_BUTTON,
TAB_BAR_WALLET_BUTTON,
} from '../../wdio/screen-objects/testIDs/Components/TabBar.testIds';
@@ -18,4 +19,8 @@ export default class TabBarComponent {
static async tapActions() {
await TestHelpers.waitAndTap(TAB_BAR_ACTION_BUTTON);
}
+
+ static async tapSettings() {
+ await TestHelpers.waitAndTap(TAB_BAR_SETTING_BUTTON);
+ }
}
diff --git a/e2e/pages/TestDApp.js b/e2e/pages/TestDApp.js
new file mode 100644
index 00000000000..34e5c32b630
--- /dev/null
+++ b/e2e/pages/TestDApp.js
@@ -0,0 +1,59 @@
+import TestHelpers from '../helpers';
+import { testDappConnectButtonCooridinates } from '../viewHelper';
+import ConnectModal from './modals/ConnectModal';
+import { BROWSER_WEBVIEW_ID } from '../../app/constants/test-ids';
+import Browser from './Drawer/Browser';
+
+export const TEST_DAPP_URL = 'https://metamask.github.io/test-dapp/';
+
+const BUTTON_RELATIVE_PONT = { x: 200, y: 5 };
+
+export class TestDApp {
+ static async connect() {
+ await TestHelpers.tapAtPoint(
+ BROWSER_WEBVIEW_ID,
+ testDappConnectButtonCooridinates,
+ );
+ await ConnectModal.isVisible();
+ await ConnectModal.tapConnectButton();
+ await TestHelpers.delay(3000);
+ }
+
+ static async tapEthSignButton() {
+ await this.#tapButton('ethSign');
+ }
+
+ static async tapPersonalSignButton() {
+ await this.#tapButton('personalSign');
+ }
+
+ static async tapTypedSignButton() {
+ await this.#tapButton('signTypedData');
+ }
+
+ static async tapTypedV3SignButton() {
+ await this.#tapButton('signTypedDataV3');
+ }
+
+ static async tapTypedV4SignButton() {
+ await this.#tapButton('signTypedDataV4');
+ }
+
+ // All the below functions are temporary until Detox supports webview interaction in iOS.
+
+ static async #tapButton(elementId) {
+ await this.#scrollToButton(elementId);
+ await TestHelpers.tapAtPoint(BROWSER_WEBVIEW_ID, BUTTON_RELATIVE_PONT);
+ await TestHelpers.delay(3000);
+ }
+
+ static async #scrollToButton(buttonId) {
+ await Browser.tapUrlInputBox();
+
+ await Browser.navigateToURL(
+ `${TEST_DAPP_URL}?scrollTo=${buttonId}&time=${Date.now()}`,
+ );
+
+ await TestHelpers.delay(3000);
+ }
+}
diff --git a/e2e/pages/TransactionConfirmView.js b/e2e/pages/TransactionConfirmView.js
index 54c37973a17..1c47647fc25 100644
--- a/e2e/pages/TransactionConfirmView.js
+++ b/e2e/pages/TransactionConfirmView.js
@@ -8,7 +8,7 @@ import {
} from '../../wdio/screen-objects/testIDs/Screens/TransactionConfirm.testIds';
import { ESTIMATED_FEE_TEST_ID } from '../../wdio/screen-objects/testIDs/Screens/TransactionSummaryScreen.testIds.js';
import {
- EDIT_PRIOTIRY_SCREEN_TEST_ID,
+ EDIT_PRIORITY_SCREEN_TEST_ID,
MAX_PRIORITY_FEE_INPUT_TEST_ID,
} from '../../wdio/screen-objects/testIDs/Screens/EditGasFeeScreen.testids.js';
@@ -46,7 +46,7 @@ export default class TransactionConfirmationView {
}
static async isPriorityEditScreenVisible() {
- await TestHelpers.checkIfVisible(EDIT_PRIOTIRY_SCREEN_TEST_ID);
+ await TestHelpers.checkIfVisible(EDIT_PRIORITY_SCREEN_TEST_ID);
}
static async isMaxPriorityFeeCorrect(amount) {
diff --git a/e2e/pages/WalletView.js b/e2e/pages/WalletView.js
index fbb81796f20..35c76c165ed 100644
--- a/e2e/pages/WalletView.js
+++ b/e2e/pages/WalletView.js
@@ -14,7 +14,6 @@ import {
import { NOTIFICATION_TITLE } from '../../wdio/screen-objects/testIDs/Components/Notification.testIds';
const WALLET_CONTAINER_ID = 'wallet-screen';
-const DRAWER_BUTTON_ID = 'hamburger-menu-button-wallet';
const NETWORK_NAME_TEXT_ID = 'network-name';
const NFT_CONTAINER_ID = 'collectible-name';
@@ -31,19 +30,6 @@ export default class WalletView {
await TestHelpers.tap(WALLET_ACCOUNT_ICON);
}
- static async tapDrawerButton() {
- await TestHelpers.tap(DRAWER_BUTTON_ID);
- }
-
- static async tapBrowser() {
- await TestHelpers.tapByText('Browser');
- await TestHelpers.delay(1000);
- }
-
- static async tapWallet() {
- await TestHelpers.tapByText('Wallet');
- }
-
static async tapSendIcon() {
await TestHelpers.waitAndTap(SEND_BUTTON_ID);
}
diff --git a/e2e/pages/modals/AddAccountModal.js b/e2e/pages/modals/AddAccountModal.js
new file mode 100644
index 00000000000..a5140729ca7
--- /dev/null
+++ b/e2e/pages/modals/AddAccountModal.js
@@ -0,0 +1,8 @@
+import TestHelpers from '../../helpers';
+import { ADD_ACCOUNT_NEW_ACCOUNT_BUTTON } from '../../../wdio/screen-objects/testIDs/Components/AddAccountModal.testIds';
+
+export default class AddAccountModal {
+ static async tapAddNewAccount() {
+ await TestHelpers.waitAndTap(ADD_ACCOUNT_NEW_ACCOUNT_BUTTON);
+ }
+}
diff --git a/e2e/pages/modals/AddAddressModal.js b/e2e/pages/modals/AddAddressModal.js
index 1cce66fd201..108fcedb49f 100644
--- a/e2e/pages/modals/AddAddressModal.js
+++ b/e2e/pages/modals/AddAddressModal.js
@@ -3,6 +3,10 @@ import {
ENTER_ALIAS_INPUT_BOX_ID,
ADD_ADDRESS_MODAL_CONTAINER_ID,
} from '../../../app/constants/test-ids';
+import {
+ ADDRESS_ALIAS_SAVE_BUTTON_ID,
+ ADDRESS_ALIAS_TITLE_ID,
+} from '../../../wdio/screen-objects/testIDs/Screens/AddressBook.testids';
export default class AddAddressModal {
static async typeInAlias(name) {
@@ -15,7 +19,11 @@ export default class AddAddressModal {
}
static async tapSaveButton() {
- await TestHelpers.tapByText('Save');
+ await TestHelpers.waitAndTap(ADDRESS_ALIAS_SAVE_BUTTON_ID);
+ }
+
+ static async tapTitle() {
+ await TestHelpers.waitAndTap(ADDRESS_ALIAS_TITLE_ID);
}
static async isVisible() {
diff --git a/e2e/pages/modals/ConnectModal.js b/e2e/pages/modals/ConnectModal.js
index 8bd075f9141..a1ceaae6f20 100644
--- a/e2e/pages/modals/ConnectModal.js
+++ b/e2e/pages/modals/ConnectModal.js
@@ -4,12 +4,11 @@ import {
CANCEL_BUTTON_ID,
CONNECT_BUTTON_ID,
} from '../../../app/constants/test-ids';
+import { ACCOUNT_LIST_ADD_BUTTON_ID } from '../../../wdio/screen-objects/testIDs/Components/AccountListComponent.testIds';
import messages from '../../../locales/languages/en.json';
const CONNECT_MULTIPLE_ACCOUNTS_STRING =
messages.accounts.connect_multiple_accounts;
-const CONNECT_MULTIPLE_ACCOUNTS_CREATE_ACCOUNT_TEXT =
- messages.accounts.create_new_account;
const CONNECT_MULTIPLE_ACCOUNTS_IMPORT_ACCOUNT_TEXT =
messages.accounts.import_account;
@@ -29,8 +28,8 @@ export default class ConnectModal {
await TestHelpers.tapByText(CONNECT_MULTIPLE_ACCOUNTS_STRING);
}
- static async tapCreateAccountButton() {
- await TestHelpers.tapByText(CONNECT_MULTIPLE_ACCOUNTS_CREATE_ACCOUNT_TEXT);
+ static async tapAddAccountButton() {
+ await TestHelpers.waitAndTap(ACCOUNT_LIST_ADD_BUTTON_ID);
}
static async tapImportAccountButton() {
diff --git a/e2e/pages/modals/RequestPaymentModal.js b/e2e/pages/modals/RequestPaymentModal.js
index faf3e607d31..e4d8cf611d2 100644
--- a/e2e/pages/modals/RequestPaymentModal.js
+++ b/e2e/pages/modals/RequestPaymentModal.js
@@ -3,11 +3,10 @@ import TestHelpers from '../../helpers';
const REQUEST_PAYMENT_CONTAINER_ID = 'receive-request-screen';
const REQUEST_BUTTON_ID = 'request-payment-button';
const PUBLIC_ADDRESS_ID = 'account-address';
-//const ADD_FUNDS_BUTTON_ID = 'drawer-receive-button';
export default class RequestPaymentModal {
static async tapRequestPaymentButton() {
- await TestHelpers.tap(REQUEST_BUTTON_ID);
+ await TestHelpers.waitAndTap(REQUEST_BUTTON_ID);
}
static async closeRequestModal() {
diff --git a/e2e/pages/modals/SigningModal.js b/e2e/pages/modals/SigningModal.js
new file mode 100644
index 00000000000..d41240b31e0
--- /dev/null
+++ b/e2e/pages/modals/SigningModal.js
@@ -0,0 +1,36 @@
+import TestHelpers from '../../helpers';
+import {
+ SIGNATURE_MODAL_CANCEL_BUTTON_ID,
+ SIGNATURE_MODAL_ETH_ID,
+ SIGNATURE_MODAL_PERSONAL_ID,
+ SIGNATURE_MODAL_SIGN_BUTTON_ID,
+ SIGNATURE_MODAL_TYPED_ID,
+} from '../../../app/constants/test-ids';
+
+export default class SigningModal {
+ static async tapSignButton() {
+ await TestHelpers.tap(SIGNATURE_MODAL_SIGN_BUTTON_ID);
+ }
+
+ static async tapCancelButton() {
+ await TestHelpers.tap(SIGNATURE_MODAL_CANCEL_BUTTON_ID);
+ }
+
+ static async isEthRequestVisible() {
+ await TestHelpers.checkIfVisible(SIGNATURE_MODAL_ETH_ID);
+ }
+
+ static async isPersonalRequestVisible() {
+ await TestHelpers.checkIfVisible(SIGNATURE_MODAL_PERSONAL_ID);
+ }
+
+ static async isTypedRequestVisible() {
+ await TestHelpers.checkIfVisible(SIGNATURE_MODAL_TYPED_ID);
+ }
+
+ static async isNotVisible() {
+ await TestHelpers.checkIfNotVisible(SIGNATURE_MODAL_ETH_ID);
+ await TestHelpers.checkIfNotVisible(SIGNATURE_MODAL_PERSONAL_ID);
+ await TestHelpers.checkIfNotVisible(SIGNATURE_MODAL_TYPED_ID);
+ }
+}
diff --git a/e2e/pages/modals/WalletActionsModal.js b/e2e/pages/modals/WalletActionsModal.js
index 2c58c24aac5..69cd5aaec0a 100644
--- a/e2e/pages/modals/WalletActionsModal.js
+++ b/e2e/pages/modals/WalletActionsModal.js
@@ -1,8 +1,15 @@
import TestHelpers from '../../helpers';
-import { WALLET_SEND_ACTION_BUTTON } from '../../../wdio/screen-objects/testIDs/Components/WalletActionModal.testIds';
+import {
+ WALLET_RECEIVE_ACTION_BUTTON,
+ WALLET_SEND_ACTION_BUTTON,
+} from '../../../wdio/screen-objects/testIDs/Components/WalletActionModal.testIds';
export default class WalletActionsModal {
static async tapSendButton() {
await TestHelpers.waitAndTap(WALLET_SEND_ACTION_BUTTON);
}
+
+ static async tapReceiveButton() {
+ await TestHelpers.waitAndTap(WALLET_RECEIVE_ACTION_BUTTON);
+ }
}
diff --git a/e2e/specs/add-custom-rpc.spec.js b/e2e/specs/add-custom-rpc.spec.js
index 0923d20479b..7ba4bf8c754 100644
--- a/e2e/specs/add-custom-rpc.spec.js
+++ b/e2e/specs/add-custom-rpc.spec.js
@@ -11,7 +11,6 @@ import NetworkView from '../pages/Drawer/Settings/NetworksView';
import MetaMetricsOptIn from '../pages/Onboarding/MetaMetricsOptInView';
import WalletView from '../pages/WalletView';
-import DrawerView from '../pages/Drawer/DrawerView';
import SettingsView from '../pages/Drawer/Settings/SettingsView';
import NetworkListModal from '../pages/modals/NetworkListModal';
@@ -23,6 +22,7 @@ import ProtectYourWalletModal from '../pages/modals/ProtectYourWalletModal';
import WhatsNewModal from '../pages/modals/WhatsNewModal';
import EnableAutomaticSecurityChecksView from '../pages/EnableAutomaticSecurityChecksView';
import { acceptTermOfUse } from '../viewHelper';
+import TabBarComponent from '../pages/TabBarComponent';
const GORELI = 'Goerli Test Network';
const XDAI_URL = 'https://rpc.gnosischain.com';
@@ -105,14 +105,8 @@ describe(Regression('Custom RPC Tests'), () => {
});
it('should go to settings then networks', async () => {
- // Open Drawer
- await WalletView.tapDrawerButton(); // tapping burger menu
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapNetworks();
-
await NetworkView.isNetworkViewVisible();
});
@@ -178,15 +172,12 @@ describe(Regression('Custom RPC Tests'), () => {
});
it('should go to settings networks and remove xDai network', async () => {
- // Open Drawer
- await WalletView.tapDrawerButton(); // tapping burger menu
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapNetworks();
await NetworkView.isNetworkViewVisible();
await NetworkView.removeNetwork(); // Tap on xDai to remove network
+ await NetworkEducationModal.tapGotItButton();
await NetworkView.tapBackButtonAndReturnToWallet();
await WalletView.isVisible();
diff --git a/e2e/specs/addressbook-tests.spec.js b/e2e/specs/addressbook-tests.spec.js
index 904f0530977..b5751bfa241 100644
--- a/e2e/specs/addressbook-tests.spec.js
+++ b/e2e/specs/addressbook-tests.spec.js
@@ -9,7 +9,6 @@ import SendView from '../pages/SendView';
import MetaMetricsOptIn from '../pages/Onboarding/MetaMetricsOptInView';
import WalletView from '../pages/WalletView';
-import DrawerView from '../pages/Drawer/DrawerView';
import SettingsView from '../pages/Drawer/Settings/SettingsView';
import ContactsView from '../pages/Drawer/Settings/Contacts/ContactsView';
@@ -25,6 +24,8 @@ import EnableAutomaticSecurityChecksView from '../pages/EnableAutomaticSecurityC
import TestHelpers from '../helpers';
import { acceptTermOfUse } from '../viewHelper';
+import TabBarComponent from '../pages/TabBarComponent';
+import WalletActionsModal from '../pages/modals/WalletActionsModal';
const INVALID_ADDRESS = '0xB8B4EE5B1b693971eB60bDa15211570df2dB221L';
const TETHER_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7';
@@ -107,11 +108,8 @@ describe(Smoke('Addressbook Tests'), () => {
});
it('should go to send view', async () => {
- // Open Drawer
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapSendButton();
+ await TabBarComponent.tapActions();
+ await WalletActionsModal.tapSendButton();
// Make sure view with my accounts visible
await SendView.isTransferBetweenMyAccountsButtonVisible();
});
@@ -132,6 +130,7 @@ describe(Smoke('Addressbook Tests'), () => {
await AddAddressModal.isVisible();
await AddAddressModal.typeInAlias('Myth');
+ await AddAddressModal.tapTitle();
await AddAddressModal.tapSaveButton();
await SendView.removeAddress();
@@ -139,15 +138,8 @@ describe(Smoke('Addressbook Tests'), () => {
});
it('should go to settings then select contacts', async () => {
- await SendView.tapcancelButton();
-
- // Check that we are on the wallet screen
- await WalletView.isVisible();
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await SendView.tapCancelButton();
+ await TabBarComponent.tapSettings();
await SettingsView.tapContacts();
await ContactsView.isVisible();
@@ -205,13 +197,11 @@ describe(Smoke('Addressbook Tests'), () => {
it('should go back to send flow to validate newly added address is displayed', async () => {
// tap on the back arrow
await AddContactView.tapBackButton();
- await SettingsView.tapCloseButton();
+ await TabBarComponent.tapWallet();
await WalletView.isVisible();
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapSendButton();
+ await TabBarComponent.tapActions();
+ await WalletActionsModal.tapSendButton();
await SendView.isSavedAliasVisible('Ibrahim');
});
diff --git a/e2e/specs/confirmations/send-multisig-address-ios.spec.js b/e2e/specs/confirmations/send-multisig-address-ios.spec.js
index 4e1cd00145b..3b00287744a 100644
--- a/e2e/specs/confirmations/send-multisig-address-ios.spec.js
+++ b/e2e/specs/confirmations/send-multisig-address-ios.spec.js
@@ -2,7 +2,6 @@
import { Regression } from '../../tags';
import WalletView from '../../pages/WalletView';
-import DrawerView from '../../pages/Drawer/DrawerView';
import SendView from '../../pages/SendView';
import AmountView from '../../pages/AmountView';
import TransactionConfirmationView from '../../pages/TransactionConfirmView';
@@ -10,6 +9,8 @@ import {
importWalletWithRecoveryPhrase,
switchToGoreliNetwork,
} from '../../viewHelper';
+import TabBarComponent from '../../pages/TabBarComponent';
+import WalletActionsModal from '../../pages/modals/WalletActionsModal';
const MULTISIG_ADDRESS = '0x0C1DD822d1Ddf78b0b702df7BF9fD0991D6255A1';
@@ -23,11 +24,8 @@ describe(Regression('Send to multisig address on iOS'), () => {
await switchToGoreliNetwork();
// Check that we are on the wallet screen
await WalletView.isVisible();
- // Open Drawer
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapSendButton();
+ await TabBarComponent.tapActions();
+ await WalletActionsModal.tapSendButton();
await SendView.inputAddress(MULTISIG_ADDRESS);
await SendView.tapNextButton();
diff --git a/e2e/specs/confirmations/sign-messages.spec.js b/e2e/specs/confirmations/sign-messages.spec.js
new file mode 100644
index 00000000000..56acad74f80
--- /dev/null
+++ b/e2e/specs/confirmations/sign-messages.spec.js
@@ -0,0 +1,129 @@
+'use strict';
+import Browser from '../../pages/Drawer/Browser';
+import TabBarComponent from '../../pages/TabBarComponent';
+import { importWalletWithRecoveryPhrase } from '../../viewHelper';
+import SigningModal from '../../pages/modals/SigningModal';
+import { TestDApp } from '../../pages/TestDApp';
+import SettingsView from '../../pages/Drawer/Settings/SettingsView';
+import AdvancedSettingsView from '../../pages/Drawer/Settings/AdvancedView';
+import { Smoke } from '../../tags';
+import TestHelpers from '../../helpers';
+
+const MAX_ATTEMPTS = 3;
+
+describe(Smoke('Sign Messages'), () => {
+ beforeAll(async () => {
+ jest.setTimeout(150000);
+ });
+
+ it('should import wallet and go to the wallet view', async () => {
+ await importWalletWithRecoveryPhrase();
+ });
+
+ it('should navigate to browser', async () => {
+ await TabBarComponent.tapBrowser();
+ await Browser.isVisible();
+ });
+
+ it('should connect to the test dapp', async () => {
+ await Browser.navigateToTestDApp();
+ await TestDApp.connect();
+ });
+
+ it('should sign personal message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapPersonalSignButton();
+ await SigningModal.isPersonalRequestVisible();
+ await SigningModal.tapSignButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should cancel personal message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapPersonalSignButton();
+ await SigningModal.isPersonalRequestVisible();
+ await SigningModal.tapCancelButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should sign typed message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapTypedSignButton();
+ await SigningModal.isTypedRequestVisible();
+ await SigningModal.tapSignButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should cancel typed message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapTypedSignButton();
+ await SigningModal.isTypedRequestVisible();
+ await SigningModal.tapCancelButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should sign typed V3 message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapTypedV3SignButton();
+ await SigningModal.isTypedRequestVisible();
+ await SigningModal.tapSignButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should cancel typed V3 message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapTypedV3SignButton();
+ await SigningModal.isTypedRequestVisible();
+ await SigningModal.tapCancelButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should sign typed V4 message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapTypedV4SignButton();
+ await SigningModal.isTypedRequestVisible();
+ await SigningModal.tapSignButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should cancel typed V4 message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapTypedV4SignButton();
+ await SigningModal.isTypedRequestVisible();
+ await SigningModal.tapCancelButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should allow eth_sign in advanced settings', async () => {
+ await TabBarComponent.tapSettings();
+ await SettingsView.tapAdvanced();
+ await AdvancedSettingsView.tapEthSignSwitch();
+ await TabBarComponent.tapBrowser();
+ });
+
+ it('should sign eth_sign message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapEthSignButton();
+ await SigningModal.isEthRequestVisible();
+ await SigningModal.tapSignButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+
+ it('should cancel eth_sign message', async () => {
+ await TestHelpers.retry(MAX_ATTEMPTS, async () => {
+ await TestDApp.tapEthSignButton();
+ await SigningModal.isEthRequestVisible();
+ await SigningModal.tapCancelButton();
+ await SigningModal.isNotVisible();
+ });
+ });
+});
diff --git a/e2e/specs/contract-nickname.failing.js b/e2e/specs/contract-nickname.failing.js
index 6eca3da459d..794486e6614 100644
--- a/e2e/specs/contract-nickname.failing.js
+++ b/e2e/specs/contract-nickname.failing.js
@@ -6,7 +6,6 @@ import OnboardingCarouselView from '../pages/Onboarding/OnboardingCarouselView';
import ContractNickNameView from '../pages/ContractNickNameView';
import SendView from '../pages/SendView';
-import DrawerView from '../pages/Drawer/DrawerView';
import MetaMetricsOptIn from '../pages/Onboarding/MetaMetricsOptInView';
import WalletView from '../pages/WalletView';
import EnableAutomaticSecurityChecksView from '../pages/EnableAutomaticSecurityChecksView';
@@ -26,6 +25,8 @@ import SecurityAndPrivacy from '../pages/Drawer/Settings/SecurityAndPrivacy/Secu
import TestHelpers from '../helpers';
import { acceptTermOfUse } from '../viewHelper';
import Accounts from '../../wdio/helpers/Accounts';
+import TabBarComponent from '../pages/TabBarComponent';
+import WalletActionsModal from '../pages/modals/WalletActionsModal';
describe('Adding Contract Nickname', () => {
const APPROVAL_DEEPLINK_URL =
@@ -109,11 +110,7 @@ describe('Adding Contract Nickname', () => {
await NetworkEducationModal.isNotVisible();
});
it('should go to the Privacy and settings view', async () => {
- await WalletView.tapDrawerButton(); // tapping burger menu
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapSecurityAndPrivacy();
await SecurityAndPrivacy.scrollToTurnOnRememberMe();
@@ -174,11 +171,7 @@ describe('Adding Contract Nickname', () => {
it('should verify contract does not appear in contacts view', async () => {
// Check that we are on the wallet screen
await WalletView.isVisible();
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapContacts();
await ContactsView.isVisible();
@@ -190,10 +183,8 @@ describe('Adding Contract Nickname', () => {
await AddContactView.tapBackButton();
await SettingsView.tapCloseButton();
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapSendButton();
+ await TabBarComponent.tapActions();
+ await WalletActionsModal.tapSendButton();
// Make sure view with my accounts visible
await SendView.isTransferBetweenMyAccountsButtonVisible();
});
@@ -213,11 +204,8 @@ describe('Adding Contract Nickname', () => {
});
it('should go to the send view again', async () => {
- // Open Drawer
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapSendButton();
+ await TabBarComponent.tapActions();
+ await WalletActionsModal.tapSendButton();
// Make sure view with my accounts visible
await SendView.isTransferBetweenMyAccountsButtonVisible();
});
diff --git a/e2e/specs/deeplinks.spec.js b/e2e/specs/deeplinks.spec.js
index 74077743bba..e1b81b90438 100644
--- a/e2e/specs/deeplinks.spec.js
+++ b/e2e/specs/deeplinks.spec.js
@@ -7,7 +7,6 @@ import NetworkApprovalModal from '../pages/modals/NetworkApprovalModal';
import NetworkAddedModal from '../pages/modals/NetworkAddedModal';
import Browser from '../pages/Drawer/Browser';
-import DrawerView from '../pages/Drawer/DrawerView';
import NetworkView from '../pages/Drawer/Settings/NetworksView';
import SettingsView from '../pages/Drawer/Settings/SettingsView';
import LoginView from '../pages/LoginView';
@@ -18,6 +17,7 @@ import SecurityAndPrivacy from '../pages/Drawer/Settings/SecurityAndPrivacy/Secu
import WalletView from '../pages/WalletView';
import { importWalletWithRecoveryPhrase } from '../viewHelper';
import Accounts from '../../wdio/helpers/Accounts';
+import TabBarComponent from '../pages/TabBarComponent';
const BINANCE_RPC_URL = 'https://bsc-dataseed1.binance.org';
@@ -50,11 +50,7 @@ describe(Regression('Deep linking Tests'), () => {
});
it('should go to the Privacy and settings view', async () => {
- await WalletView.tapDrawerButton(); // tapping burger menu
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapSecurityAndPrivacy();
await SecurityAndPrivacy.scrollToTurnOnRememberMe();
@@ -89,12 +85,7 @@ describe(Regression('Deep linking Tests'), () => {
});
it('should go to settings then networks', async () => {
- // Open Drawer
- await WalletView.tapDrawerButton(); // tapping burger menu
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapNetworks();
await NetworkView.isNetworkViewVisible();
diff --git a/e2e/specs/delete-wallet.spec.js b/e2e/specs/delete-wallet.spec.js
index efedf6775cf..f2a6d32dbdf 100644
--- a/e2e/specs/delete-wallet.spec.js
+++ b/e2e/specs/delete-wallet.spec.js
@@ -8,11 +8,8 @@ import OnboardingCarouselView from '../pages/Onboarding/OnboardingCarouselView';
import ImportWalletView from '../pages/Onboarding/ImportWalletView';
import MetaMetricsOptIn from '../pages/Onboarding/MetaMetricsOptInView';
-import WalletView from '../pages/WalletView';
import LoginView from '../pages/LoginView';
-import DrawerView from '../pages/Drawer/DrawerView';
-
import SettingsView from '../pages/Drawer/Settings/SettingsView';
import SecurityAndPrivacyView from '../pages/Drawer/Settings/SecurityAndPrivacy/SecurityAndPrivacyView';
@@ -24,6 +21,7 @@ import WhatsNewModal from '../pages/modals/WhatsNewModal';
import EnableAutomaticSecurityChecksView from '../pages/EnableAutomaticSecurityChecksView';
import { acceptTermOfUse } from '../viewHelper';
import Accounts from '../../wdio/helpers/Accounts';
+import TabBarComponent from '../pages/TabBarComponent';
describe.skip(
Smoke(
@@ -90,15 +88,9 @@ describe.skip(
});
it('should go to settings then security & privacy', async () => {
- // Open Drawer
- await WalletView.tapDrawerButton(); // tapping burger menu
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapSecurityAndPrivacy();
await SecurityAndPrivacyView.scrollToChangePasswordView();
-
await SecurityAndPrivacyView.isChangePasswordSectionVisible();
});
@@ -128,11 +120,9 @@ describe.skip(
await SettingsView.tapCloseButton();
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapLockAccount();
- await DrawerView.tapYesAlertButton();
+ await TabBarComponent.tapActions();
+ await SettingsView.tapLock();
+ await SettingsView.tapYesAlertButton();
await LoginView.isVisible();
});
diff --git a/e2e/specs/onboarding-wizard-opt-in.spec.js b/e2e/specs/onboarding-wizard-opt-in.spec.js
index fbee8c66db7..cb8012584bd 100644
--- a/e2e/specs/onboarding-wizard-opt-in.spec.js
+++ b/e2e/specs/onboarding-wizard-opt-in.spec.js
@@ -11,8 +11,6 @@ import MetaMetricsOptIn from '../pages/Onboarding/MetaMetricsOptInView';
import WalletView from '../pages/WalletView';
import EnableAutomaticSecurityChecksView from '../pages/EnableAutomaticSecurityChecksView';
-import DrawerView from '../pages/Drawer/DrawerView';
-
import SettingsView from '../pages/Drawer/Settings/SettingsView';
import SecurityAndPrivacy from '../pages/Drawer/Settings/SecurityAndPrivacy/SecurityAndPrivacyView';
@@ -23,6 +21,7 @@ import OnboardingWizardModal from '../pages/modals/OnboardingWizardModal';
import ProtectYourWalletModal from '../pages/modals/ProtectYourWalletModal';
import WhatsNewModal from '../pages/modals/WhatsNewModal';
import { acceptTermOfUse } from '../viewHelper';
+import TabBarComponent from '../pages/TabBarComponent';
const PASSWORD = '12345678';
@@ -101,10 +100,7 @@ describe.skip(
});
it('should check that metametrics is enabled in settings', async () => {
- await WalletView.tapDrawerButton(); // tapping burger menu
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
+ await TabBarComponent.tapSettings();
await SettingsView.tapSecurityAndPrivacy();
@@ -147,11 +143,7 @@ describe.skip(
});
it('should verify metametrics is turned off', async () => {
- await WalletView.tapDrawerButton(); // tapping burger menu
-
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapSecurityAndPrivacy();
await SecurityAndPrivacy.scrollToBottomOfView();
diff --git a/e2e/specs/permission-system-delete-wallet.spec.js b/e2e/specs/permission-system-delete-wallet.spec.js
index f8588221fb6..880a46e0d80 100644
--- a/e2e/specs/permission-system-delete-wallet.spec.js
+++ b/e2e/specs/permission-system-delete-wallet.spec.js
@@ -18,8 +18,8 @@ import SkipAccountSecurityModal from '../pages/modals/SkipAccountSecurityModal';
import ConnectedAccountsModal from '../pages/modals/ConnectedAccountsModal';
import ConnectModal from '../pages/modals/ConnectModal';
import DeleteWalletModal from '../pages/modals/DeleteWalletModal';
-import DrawerView from '../pages/Drawer/DrawerView';
import NetworkListModal from '../pages/modals/NetworkListModal';
+import SettingsView from '../pages/Drawer/Settings/SettingsView';
import {
importWalletWithRecoveryPhrase,
@@ -72,10 +72,9 @@ describe.skip(
});
it('should open drawer and log out', async () => {
- await WalletView.tapDrawerButton();
- await DrawerView.isVisible();
- await DrawerView.tapLockAccount();
- await DrawerView.tapYesAlertButton();
+ await TabBarComponent.tapSettings();
+ await SettingsView.tapLock();
+ await SettingsView.tapYesAlertButton();
await LoginView.isVisible();
});
diff --git a/e2e/specs/permission-system-revoking-multiple-accounts.spec.js b/e2e/specs/permission-system-revoking-multiple-accounts.spec.js
index 7455b881144..1ba1002c982 100644
--- a/e2e/specs/permission-system-revoking-multiple-accounts.spec.js
+++ b/e2e/specs/permission-system-revoking-multiple-accounts.spec.js
@@ -15,6 +15,7 @@ import {
testDappConnectButtonCooridinates,
} from '../viewHelper';
import NetworkListModal from '../pages/modals/NetworkListModal';
+import AddAccountModal from '../pages/modals/AddAccountModal';
const SUSHI_SWAP = 'https://app.sushi.com/swap';
const TEST_DAPP = 'https://metamask.github.io/test-dapp/';
@@ -64,9 +65,10 @@ describe('Connecting to multiple dapps and revoking permission on one but stayin
it('should connect with multiple accounts', async () => {
// Wait for page to load
- await ConnectModal.tapCreateAccountButton();
+ await ConnectModal.tapAddAccountButton();
+ await AddAccountModal.tapAddNewAccount();
await AccountListView.isNewAccountNameVisible();
- await AccountListView.tapAccountByName('Account 2');
+ await AccountListView.tapNewAccount2();
await ConnectModal.tapAccountConnectMultiSelectButton();
});
diff --git a/e2e/specs/request-token-flow.spec.js b/e2e/specs/request-token-flow.spec.js
index 8e3a1bb10de..a89d5e2fe25 100644
--- a/e2e/specs/request-token-flow.spec.js
+++ b/e2e/specs/request-token-flow.spec.js
@@ -11,7 +11,6 @@ import RequestPaymentView from '../pages/RequestPaymentView';
import MetaMetricsOptIn from '../pages/Onboarding/MetaMetricsOptInView';
import WalletView from '../pages/WalletView';
-import DrawerView from '../pages/Drawer/DrawerView';
import EnableAutomaticSecurityChecksView from '../pages/EnableAutomaticSecurityChecksView';
import SkipAccountSecurityModal from '../pages/modals/SkipAccountSecurityModal';
@@ -22,6 +21,8 @@ import WhatsNewModal from '../pages/modals/WhatsNewModal';
import TestHelpers from '../helpers';
import { acceptTermOfUse } from '../viewHelper';
+import TabBarComponent from '../pages/TabBarComponent';
+import WalletActionsModal from '../pages/modals/WalletActionsModal';
const SAI_CONTRACT_ADDRESS = '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359';
const PASSWORD = '12345678';
@@ -101,11 +102,8 @@ describe(Smoke('Request Token Flow'), () => {
});
it('should go to send view', async () => {
- await WalletView.tapDrawerButton();
-
- await DrawerView.isVisible();
- await DrawerView.tapOnAddFundsButton();
- // Check that we see the receive modal
+ await TabBarComponent.tapActions();
+ await WalletActionsModal.tapReceiveButton();
await RequestPaymentModal.isVisible();
});
diff --git a/e2e/specs/send-ERC-token.spec.js b/e2e/specs/send-ERC-token.spec.js
index 255c6d35b83..59216ff6b79 100644
--- a/e2e/specs/send-ERC-token.spec.js
+++ b/e2e/specs/send-ERC-token.spec.js
@@ -4,7 +4,6 @@ import { Smoke } from '../tags';
import TestHelpers from '../helpers';
import WalletView from '../pages/WalletView';
-import DrawerView from '../pages/Drawer/DrawerView';
import SettingsView from '../pages/Drawer/Settings/SettingsView';
import NetworkView from '../pages/Drawer/Settings/NetworksView';
import NetworkEducationModal from '../pages/modals/NetworkEducationModal';
@@ -13,6 +12,7 @@ import SendView from '../pages/SendView';
import AmountView from '../pages/AmountView';
import { importWalletWithRecoveryPhrase } from '../viewHelper';
import TransactionConfirmationView from '../pages/TransactionConfirmView';
+import TabBarComponent from '../pages/TabBarComponent';
const AVAX_URL = 'https://api.avax-test.network/ext/C/rpc';
const TOKEN_ADDRESS = '0x5425890298aed601595a70AB815c96711a31Bc65';
@@ -28,9 +28,7 @@ describe(Smoke('Send ERC Token'), () => {
});
it('should add AVAX testnet to my networks list', async () => {
- await WalletView.tapDrawerButton(); // tapping burger menu
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
+ await TabBarComponent.tapSettings();
await SettingsView.tapNetworks();
await NetworkView.isNetworkViewVisible();
diff --git a/e2e/viewHelper.js b/e2e/viewHelper.js
index 1f69086bf40..c32489ead69 100644
--- a/e2e/viewHelper.js
+++ b/e2e/viewHelper.js
@@ -1,6 +1,5 @@
'use strict';
-import DrawerView from './pages/Drawer/DrawerView';
import EnableAutomaticSecurityChecksView from './pages/EnableAutomaticSecurityChecksView';
import ImportWalletView from './pages/Onboarding/ImportWalletView';
import MetaMetricsOptIn from './pages/Onboarding/MetaMetricsOptInView';
@@ -18,6 +17,7 @@ import Accounts from '../wdio/helpers/Accounts';
import TestHelpers from './helpers';
import TermsOfUseModal from './pages/modals/TermsOfUseModal';
+import TabBarComponent from './pages/TabBarComponent';
const GOERLI = 'Goerli Test Network';
@@ -81,12 +81,8 @@ export const importWalletWithRecoveryPhrase = async () => {
};
export const addLocalhostNetwork = async () => {
- await WalletView.tapDrawerButton();
- await DrawerView.isVisible();
- await DrawerView.tapSettings();
-
+ await TabBarComponent.tapSettings();
await SettingsView.tapNetworks();
-
await NetworkView.isNetworkViewVisible();
await TestHelpers.delay(3000);
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 9cf0a30cfd7..37df2961656 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -777,7 +777,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
- Branch: 4ac024cb3c29b0ef628048694db3c4cfa679beb0
+ Branch: 74cc856025984f691833c8fa332834ac38a0cf4e
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
diff --git a/locales/languages/en.json b/locales/languages/en.json
index ccaf5e3cd78..4df12c9200f 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -496,20 +496,30 @@
"unselect_all": "Unselect all",
"permissions": "Permissions",
"revoke": "Revoke",
- "revoke_all": "Revoke all"
+ "revoke_all": "Revoke all",
+ "site_permission_to": "This site has permission to:",
+ "address_balance_activity_permission": "See address, account balance, and activity",
+ "suggest_transactions": "Suggest transactions to approve",
+ "disconnect": "Disconnect",
+ "disconnect_all_accounts": "Disconnect all accounts"
},
"toast": {
"connected_and_active": "connected and active.",
"now_active": "now active.",
"revoked": "revoked.",
"revoked_all": "All accounts revoked.",
- "accounts_connected": "accounts connected."
+ "accounts_connected": "accounts connected.",
+ "disconnected": "disconnected.",
+ "disconnected_all": "All accounts disconected."
},
"connect_qr_hardware": {
"title": "Connect a QR-based hardware wallet",
"description1": "Connect an airgapped hardware wallet that communicates through QR-codes.",
"description2": "How it works?",
"description3": "Officially supported airgapped hardware wallets include:",
+ "keystone": "Keystone",
+ "learnMore": "Learn more",
+ "tutorial": "Tutorial",
"description4": "Keystone (tutorial)",
"description5": "1. Unlock your Keystone",
"description6": "2. Tap the ··· Menu, then go to Sync >",
@@ -589,6 +599,7 @@
"toggleEthSignBannerDescription": "You’re at risk for phishing attacks. Protect yourself by turning off eth_sign.",
"enable_eth_sign": "Eth_sign requests",
"enable_eth_sign_desc": "If you enable this setting, you might get signature requests that aren’t readable. By signing a message you don't understand, you could be agreeing to give away your funds and NFTs.",
+ "enable_eth_sign_warning": "You're at risk for phishing attacks. Protect yourself by turning off eth_sign.",
"toggleEthSignModalBannerText": "If you've been asked to turn this setting on,",
"toggleEthSignModalBannerBoldText": " you might be getting scammed.",
"toggleEthSignModalCheckBox": "I understand that I can lose all of my funds and NFTs if I enable eth_sign requests.",
@@ -662,6 +673,8 @@
"clear_cookies_desc": "Choose this option to clear your browser's cookies.",
"metametrics_title": "Participate in MetaMetrics",
"metametrics_description": "Participate in MetaMetrics to help us make MetaMask better.",
+ "batch_balance_requests_title": "Batch account balance requests",
+ "batch_balance_requests_description": "We batch accounts and query Infura to responsively show your balances. If you turn this off, only active accounts will be queried. Some dApps won’t work unless you connect your wallet.",
"third_party_title": "Get incoming transactions",
"third_party_description": "Third party APIs (Etherscan) are used to show your incoming transactions in the history. Turn off if you don’t want us to pull data from those services.",
"metametrics_opt_out": "MetaMetrics Opt-out",
@@ -731,7 +744,9 @@
"paste_or_type_activation_key": "Paste or type an Activation Key",
"add": "Add",
"cancel": "Cancel"
- }
+ },
+ "request_feature": "Request a feature",
+ "contact_support": "Contact support"
},
"sdk": {
"disconnect_title": "Disconnect all sites?",
@@ -1081,6 +1096,8 @@
"approve": "Approve",
"allow_to_access": "Give permission to access your",
"allow_to_address_access": "Give this address access your",
+ "set_spend_cap": "Set a spending cap for your",
+ "review_spend_cap": "Review your spending cap",
"token": "token",
"nft": "NFT",
"you_trust_this_site": "By granting permission, you're allowing the following third party to access your funds.",
@@ -1089,6 +1106,7 @@
"edit": "Edit",
"transaction_fee_explanation": "A transaction fee is associated with this permission.",
"view_details": "View details",
+ "view_transaction_details": "View transaction details",
"view_data": "View Data",
"transaction_details": "Transaction Details",
"site_url": "Site URL",
@@ -1299,7 +1317,8 @@
"add_other_network_here": "here.",
"you_can": "Or you can",
"add_network": "add more networks manually.",
- "select_network": "Select a network"
+ "select_network": "Select a network",
+ "show_test_networks": "Show test networks"
},
"select": {
"cancel": "Cancel",
@@ -2241,6 +2260,8 @@
"very_likely": "Very likely in <",
"at_least": "At least",
"less_than": "Less than",
+ "warning_very_likely": "This gas fee is much higher than the other options available.",
+ "warning_very_likely_title": "Gas fees too high",
"warning_unknown": "Your max fee or max priority fee may be low for current market conditions. We don’t know when (or if) your transaction will be processed.",
"warning_low": "This lowers your maximum fee but if network traffic increases your transaction may be delayed or fail.",
"warning_low_title": "Low priority",
@@ -2349,7 +2370,8 @@
"info_modal_description_default": "The contract could spend your entire token balance without further notice or consent. Protect yourself by setting a lower spending cap.",
"set_spend_cap": "Set a spending cap",
"be_careful": "Be careful",
- "error_enter_number": "Error: Enter only numbers"
+ "error_enter_number": "Error: Enter only numbers",
+ "enter_number": "Enter a number here"
},
"token_allowance": {
"verify_third_party_details": "Verify third party details",
diff --git a/locales/languages/es.json b/locales/languages/es.json
index 3083ef3f05f..97c23bea676 100644
--- a/locales/languages/es.json
+++ b/locales/languages/es.json
@@ -807,7 +807,7 @@
"seed_warning": "Esta es la frase de 12 palabras de su cartera. Esta frase se puede usar para controlar todas sus cuentas actuales y futuras, incluida la capacidad de transferir sus fondos. Guarde esta frase en un lugar seguro y NO la comparta con nadie.",
"text": "TEXTO",
"qr_code": "CÓDIGO QR",
- "hold_to_reveal_credential": "No revele su {{credentialName}}",
+ "hold_to_reveal_credential": "Mantén presionado para revelar su {{credentialName}}",
"keep_credential_safe": "Proteja su {{credentialName}}",
"srp_abbreviation_text": "SRP",
"srp_text": "Frase secreta de recuperación",
diff --git a/package.json b/package.json
index cb293c06d14..de5c6ded87c 100644
--- a/package.json
+++ b/package.json
@@ -410,6 +410,7 @@
"metro": "0.72.3",
"metro-react-native-babel-preset": "0.72.3",
"multiple-cucumber-html-reporter": "^3.0.1",
+ "nock": "^13.3.1",
"octonode": "0.10.2",
"patch-package": "^6.2.2",
"prettier": "^2.2.1",
diff --git a/patches/@metamask+assets-controllers+5.0.0.patch b/patches/@metamask+assets-controllers+5.0.0.patch
index 1d6da83e560..b54a4bc18c3 100644
--- a/patches/@metamask+assets-controllers+5.0.0.patch
+++ b/patches/@metamask+assets-controllers+5.0.0.patch
@@ -1,3 +1,80 @@
+diff --git a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.js b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.js
+index 130e3dc..912c84c 100644
+--- a/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.js
++++ b/node_modules/@metamask/assets-controllers/dist/AccountTrackerController.js
+@@ -30,7 +30,7 @@ class AccountTrackerController extends base_controller_1.BaseController {
+ * @param config - Initial options used to configure this controller.
+ * @param state - Initial state to set on this controller.
+ */
+- constructor({ onPreferencesStateChange, getIdentities, }, config, state) {
++ constructor({ onPreferencesStateChange, getIdentities, getSelectedAddress, getMultiAccountBalancesEnabled, }, config, state) {
+ super(config, state);
+ this.mutex = new async_mutex_1.Mutex();
+ /**
+@@ -38,16 +38,26 @@ class AccountTrackerController extends base_controller_1.BaseController {
+ */
+ this.name = 'AccountTrackerController';
+ /**
+- * Refreshes all accounts in the current keychain.
++ * Refreshes the balances of the accounts depending on the multi-account setting.
++ * If multi-account is disabled, only updates the selected account balance.
++ * If multi-account is enabled, updates balances for all accounts.
++ *
++ * @async
+ */
+ this.refresh = () => __awaiter(this, void 0, void 0, function* () {
+ this.syncAccounts();
+ const accounts = Object.assign({}, this.state.accounts);
++ const isMultiAccountBalancesEnabled = this.getMultiAccountBalancesEnabled();
++ if (!isMultiAccountBalancesEnabled) {
++ const selectedAddress = this.getSelectedAddress();
++ const balance = yield this.getBalanceFromChain(selectedAddress);
++ accounts[selectedAddress] = { balance: (0, controller_utils_1.BNToHex)(balance) };
++ this.update({ accounts });
++ return;
++ }
+ for (const address in accounts) {
+- yield (0, controller_utils_1.safelyExecuteWithTimeout)(() => __awaiter(this, void 0, void 0, function* () {
+- const balance = yield (0, controller_utils_1.query)(this.ethQuery, 'getBalance', [address]);
+- accounts[address] = { balance: (0, controller_utils_1.BNToHex)(balance) };
+- }));
++ const balance = yield this.getBalanceFromChain(address);
++ accounts[address] = { balance: (0, controller_utils_1.BNToHex)(balance) };
+ }
+ this.update({ accounts });
+ });
+@@ -57,6 +67,8 @@ class AccountTrackerController extends base_controller_1.BaseController {
+ this.defaultState = { accounts: {} };
+ this.initialize();
+ this.getIdentities = getIdentities;
++ this.getSelectedAddress = getSelectedAddress;
++ this.getMultiAccountBalancesEnabled = getMultiAccountBalancesEnabled;
+ onPreferencesStateChange(() => {
+ this.refresh();
+ });
+@@ -106,6 +118,22 @@ class AccountTrackerController extends base_controller_1.BaseController {
+ }, this.config.interval);
+ });
+ }
++ /**
++ * Fetches the balance of a given address from the blockchain.
++ *
++ * @async
++ * @param {string} address - The account address to fetch the balance for.
++ * @returns {Promise} - A promise that resolves to the balance in a hex string format.
++ */
++ getBalanceFromChain(address) {
++ return __awaiter(this, void 0, void 0, function* () {
++ let balance = '0x0';
++ yield (0, controller_utils_1.safelyExecuteWithTimeout)(() => __awaiter(this, void 0, void 0, function* () {
++ balance = yield (0, controller_utils_1.query)(this.ethQuery, 'getBalance', [address]);
++ }));
++ return balance;
++ });
++ }
+ /**
+ * Sync accounts balances with some additional addresses.
+ *
diff --git a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.js b/node_modules/@metamask/assets-controllers/dist/AssetsContractController.js
index 332c87d..b634fde 100644
--- a/node_modules/@metamask/assets-controllers/dist/AssetsContractController.js
@@ -32,7 +109,7 @@ index 332c87d..b634fde 100644
* Enumerate assets assigned to an owner.
*
diff --git a/node_modules/@metamask/assets-controllers/dist/Standards/ERC20Standard.js b/node_modules/@metamask/assets-controllers/dist/Standards/ERC20Standard.js
-index 9ddbc28..b8fb35a 100644
+index 9ddbc28..ca00e3e 100644
--- a/node_modules/@metamask/assets-controllers/dist/Standards/ERC20Standard.js
+++ b/node_modules/@metamask/assets-controllers/dist/Standards/ERC20Standard.js
@@ -13,7 +13,13 @@ exports.ERC20Standard = void 0;
@@ -288,29 +365,60 @@ index e3f81e9..c3a13ac 100644
});
}
diff --git a/node_modules/@metamask/assets-controllers/dist/TokensController.js b/node_modules/@metamask/assets-controllers/dist/TokensController.js
-index 8c02fe6..53061ab 100644
+index 8c02fe6..162376f 100644
--- a/node_modules/@metamask/assets-controllers/dist/TokensController.js
+++ b/node_modules/@metamask/assets-controllers/dist/TokensController.js
-@@ -44,8 +44,9 @@ class TokensController extends base_controller_1.BaseController {
+@@ -25,13 +25,6 @@ const base_controller_1 = require("@metamask/base-controller");
+ const controller_utils_1 = require("@metamask/controller-utils");
+ const assetsUtil_1 = require("./assetsUtil");
+ const token_service_1 = require("./token-service");
+-var SuggestedAssetStatus;
+-(function (SuggestedAssetStatus) {
+- SuggestedAssetStatus["accepted"] = "accepted";
+- SuggestedAssetStatus["failed"] = "failed";
+- SuggestedAssetStatus["pending"] = "pending";
+- SuggestedAssetStatus["rejected"] = "rejected";
+-})(SuggestedAssetStatus || (SuggestedAssetStatus = {}));
+ /**
+ * Controller that stores assets and exposes convenience methods
+ */
+@@ -44,8 +37,10 @@ class TokensController extends base_controller_1.BaseController {
* @param options.onNetworkStateChange - Allows subscribing to network controller state changes.
* @param options.config - Initial options used to configure this controller.
* @param options.state - Initial state to set on this controller.
++ * @param options.messenger - The controller messenger. (Note for patch removal: This new parameter is already merged into core)
+ * @param options.getERC20TokenName - Allows fetch an ERC-20 token anme
*/
- constructor({ onPreferencesStateChange, onNetworkStateChange, config, state, }) {
-+ constructor({ onPreferencesStateChange, onNetworkStateChange, config, state, getERC20TokenName}) {
++ constructor({ onPreferencesStateChange, onNetworkStateChange, config, state, messenger, getERC20TokenName}) {
super(config, state);
this.mutex = new async_mutex_1.Mutex();
/**
-@@ -60,6 +61,7 @@ class TokensController extends base_controller_1.BaseController {
- this.defaultState = Object.assign({ tokens: [], ignoredTokens: [], detectedTokens: [], allTokens: {}, allIgnoredTokens: {}, allDetectedTokens: {}, suggestedAssets: [] }, state);
+@@ -57,9 +52,11 @@ class TokensController extends base_controller_1.BaseController {
+ */
+ this.name = 'TokensController';
+ this.defaultConfig = Object.assign({ networkType: controller_utils_1.MAINNET, selectedAddress: '', chainId: '', provider: undefined }, config);
+- this.defaultState = Object.assign({ tokens: [], ignoredTokens: [], detectedTokens: [], allTokens: {}, allIgnoredTokens: {}, allDetectedTokens: {}, suggestedAssets: [] }, state);
++ this.defaultState = Object.assign({ tokens: [], ignoredTokens: [], detectedTokens: [], allTokens: {}, allIgnoredTokens: {}, allDetectedTokens: {} }, state);
this.initialize();
this.abortController = new abort_controller_1.AbortController();
++ this.messagingSystem = messenger;
+ this.getERC20TokenName = getERC20TokenName;
onPreferencesStateChange(({ selectedAddress }) => {
var _a, _b, _c;
const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state;
-@@ -122,10 +124,11 @@ class TokensController extends base_controller_1.BaseController {
+@@ -87,10 +84,6 @@ class TokensController extends base_controller_1.BaseController {
+ });
+ });
+ }
+- failSuggestedAsset(suggestedAssetMeta, error) {
+- const failedSuggestedAssetMeta = Object.assign(Object.assign({}, suggestedAssetMeta), { status: SuggestedAssetStatus.failed, error });
+- this.hub.emit(`${suggestedAssetMeta.id}:finished`, failedSuggestedAssetMeta);
+- }
+ /**
+ * Fetch metadata for a token.
+ *
+@@ -122,10 +115,11 @@ class TokensController extends base_controller_1.BaseController {
* @param address - Hex address of the token contract.
* @param symbol - Symbol of the token.
* @param decimals - Number of decimals the token uses.
@@ -323,7 +431,7 @@ index 8c02fe6..53061ab 100644
return __awaiter(this, void 0, void 0, function* () {
const currentChainId = this.config.chainId;
const releaseLock = yield this.mutex.acquire();
-@@ -151,6 +154,7 @@ class TokensController extends base_controller_1.BaseController {
+@@ -151,6 +145,7 @@ class TokensController extends base_controller_1.BaseController {
}),
isERC721,
aggregators: (0, assetsUtil_1.formatAggregatorNames)((tokenMetadata === null || tokenMetadata === void 0 ? void 0 : tokenMetadata.aggregators) || []),
@@ -331,7 +439,7 @@ index 8c02fe6..53061ab 100644
};
const previousEntry = newTokens.find((token) => token.address.toLowerCase() === address.toLowerCase());
if (previousEntry) {
-@@ -182,6 +186,79 @@ class TokensController extends base_controller_1.BaseController {
+@@ -182,6 +177,79 @@ class TokensController extends base_controller_1.BaseController {
}
});
}
@@ -411,7 +519,7 @@ index 8c02fe6..53061ab 100644
/**
* Add a batch of tokens.
*
-@@ -199,7 +276,7 @@ class TokensController extends base_controller_1.BaseController {
+@@ -199,7 +267,7 @@ class TokensController extends base_controller_1.BaseController {
}, {});
try {
tokensToImport.forEach((tokenToAdd) => {
@@ -420,7 +528,7 @@ index 8c02fe6..53061ab 100644
const checksumAddress = (0, controller_utils_1.toChecksumHexAddress)(address);
const formattedToken = {
address: checksumAddress,
-@@ -207,6 +284,7 @@ class TokensController extends base_controller_1.BaseController {
+@@ -207,6 +275,7 @@ class TokensController extends base_controller_1.BaseController {
decimals,
image,
aggregators,
@@ -428,7 +536,7 @@ index 8c02fe6..53061ab 100644
};
newTokensMap[address] = formattedToken;
importedTokensMap[address.toLowerCase()] = true;
-@@ -282,7 +360,7 @@ class TokensController extends base_controller_1.BaseController {
+@@ -282,7 +351,7 @@ class TokensController extends base_controller_1.BaseController {
let newDetectedTokens = [...detectedTokens];
try {
incomingDetectedTokens.forEach((tokenToAdd) => {
@@ -437,7 +545,7 @@ index 8c02fe6..53061ab 100644
const checksumAddress = (0, controller_utils_1.toChecksumHexAddress)(address);
const newEntry = {
address: checksumAddress,
-@@ -291,6 +369,7 @@ class TokensController extends base_controller_1.BaseController {
+@@ -291,6 +360,7 @@ class TokensController extends base_controller_1.BaseController {
image,
isERC721,
aggregators,
@@ -445,7 +553,7 @@ index 8c02fe6..53061ab 100644
};
const previousImportedEntry = newTokens.find((token) => token.address.toLowerCase() === checksumAddress.toLowerCase());
if (previousImportedEntry) {
-@@ -357,6 +436,22 @@ class TokensController extends base_controller_1.BaseController {
+@@ -357,6 +427,22 @@ class TokensController extends base_controller_1.BaseController {
return tokens[tokenIndex];
});
}
@@ -468,7 +576,16 @@ index 8c02fe6..53061ab 100644
/**
* Detects whether or not a token is ERC-721 compatible.
*
-@@ -402,9 +497,10 @@ class TokensController extends base_controller_1.BaseController {
+@@ -396,107 +482,46 @@ class TokensController extends base_controller_1.BaseController {
+ _generateRandomId() {
+ return (0, uuid_1.v1)();
+ }
++ // THIS PATCHED METHOD HAS ALREADY BEEN RELEASED IN VERSION 8.0.0 of @metamask/assets-controllers
+ /**
+- * Adds a new suggestedAsset to state. Parameters will be validated according to
+- * asset type being watched. A `:pending` hub event will be emitted once added.
++ * Adds a new suggestedAsset to the list of watched assets.
++ * Parameters will be validated according to the asset type being watched.
*
* @param asset - The asset to be watched. For now only ERC20 tokens are accepted.
* @param type - The asset type.
@@ -478,49 +595,145 @@ index 8c02fe6..53061ab 100644
- watchAsset(asset, type) {
+ watchAsset(asset, type, interactingAddress) {
return __awaiter(this, void 0, void 0, function* () {
++ if (type !== controller_utils_1.ERC20) {
++ throw new Error(`Asset of type ${type} not supported`);
++ }
++ const { selectedAddress } = this.config;
++ const isAddingOnWalletAccount = interactingAddress ? interactingAddress === selectedAddress : true;
const suggestedAssetMeta = {
asset,
-@@ -412,6 +508,7 @@ class TokensController extends base_controller_1.BaseController {
- status: SuggestedAssetStatus.pending,
+ id: this._generateRandomId(),
+- status: SuggestedAssetStatus.pending,
time: Date.now(),
type,
-+ interactingAddress
++ interactingAddress: interactingAddress || selectedAddress,
};
- try {
- switch (type) {
-@@ -454,9 +551,12 @@ class TokensController extends base_controller_1.BaseController {
- * A `:finished` hub event is fired after accepted or failure.
- *
- * @param suggestedAssetID - The ID of the suggestedAsset to accept.
-+ * @param interactingAddress - The account address that interacted with asset that is being watched.
- */
+- try {
+- switch (type) {
+- case 'ERC20':
+- (0, assetsUtil_1.validateTokenToWatch)(asset);
+- break;
+- default:
+- throw new Error(`Asset of type ${type} not supported`);
++ (0, assetsUtil_1.validateTokenToWatch)(asset);
++ yield this._requestApproval(suggestedAssetMeta);
++ const { address, symbol, decimals, image } = suggestedAssetMeta.asset;
++ if (isAddingOnWalletAccount) {
++ let name;
++ try{
++ name = yield this.getERC20TokenName(address);
++ }catch(error){
++ name = null;
+ }
++ yield this.addToken(address, symbol, decimals, image, name);
++ } else {
++ yield this.addTokenToAccount({ accountAddress: interactingAddress, token: { address, symbol, decimals, image } });
+ }
+- catch (error) {
+- this.failSuggestedAsset(suggestedAssetMeta, error);
+- return Promise.reject(error);
+- }
+- const result = new Promise((resolve, reject) => {
+- this.hub.once(`${suggestedAssetMeta.id}:finished`, (meta) => {
+- switch (meta.status) {
+- case SuggestedAssetStatus.accepted:
+- return resolve(meta.asset.address);
+- case SuggestedAssetStatus.rejected:
+- return reject(new Error('User rejected to watch the asset.'));
+- case SuggestedAssetStatus.failed:
+- return reject(new Error(meta.error.message));
+- /* istanbul ignore next */
+- default:
+- return reject(new Error(`Unknown status: ${meta.status}`));
+- }
+- });
+- });
+- const { suggestedAssets } = this.state;
+- suggestedAssets.push(suggestedAssetMeta);
+- this.update({ suggestedAssets: [...suggestedAssets] });
+- this.hub.emit('pendingSuggestedAsset', suggestedAssetMeta);
+- return { result, suggestedAssetMeta };
+- });
+- }
+- /**
+- * Accepts to watch an asset and updates it's status and deletes the suggestedAsset from state,
+- * adding the asset to corresponding asset state. In this case ERC20 tokens.
+- * A `:finished` hub event is fired after accepted or failure.
+- *
+- * @param suggestedAssetID - The ID of the suggestedAsset to accept.
+- */
- acceptWatchAsset(suggestedAssetID) {
-+ acceptWatchAsset(suggestedAssetID, interactingAddress) {
- return __awaiter(this, void 0, void 0, function* () {
-+ const { selectedAddress } = this.config;
-+ const isAddingOnWalletAccount = interactingAddress ? interactingAddress === selectedAddress : true;
- const { suggestedAssets } = this.state;
- const index = suggestedAssets.findIndex(({ id }) => suggestedAssetID === id);
- const suggestedAssetMeta = suggestedAssets[index];
-@@ -464,7 +564,17 @@ class TokensController extends base_controller_1.BaseController {
- switch (suggestedAssetMeta.type) {
- case 'ERC20':
- const { address, symbol, decimals, image } = suggestedAssetMeta.asset;
+- return __awaiter(this, void 0, void 0, function* () {
+- const { suggestedAssets } = this.state;
+- const index = suggestedAssets.findIndex(({ id }) => suggestedAssetID === id);
+- const suggestedAssetMeta = suggestedAssets[index];
+- try {
+- switch (suggestedAssetMeta.type) {
+- case 'ERC20':
+- const { address, symbol, decimals, image } = suggestedAssetMeta.asset;
- yield this.addToken(address, symbol, decimals, image);
-+ if (isAddingOnWalletAccount) {
-+ let name;
-+ try{
-+ name = yield this.getERC20TokenName(address);
-+ }catch(error){
-+ name = null;
-+ }
-+ yield this.addToken(address, symbol, decimals, image, name);
-+ } else {
-+ yield this.addTokenToAccount({ accountAddress: interactingAddress, token: { address, symbol, decimals, image } });
-+ }
- suggestedAssetMeta.status = SuggestedAssetStatus.accepted;
- this.hub.emit(`${suggestedAssetMeta.id}:finished`, suggestedAssetMeta);
- break;
+- suggestedAssetMeta.status = SuggestedAssetStatus.accepted;
+- this.hub.emit(`${suggestedAssetMeta.id}:finished`, suggestedAssetMeta);
+- break;
+- default:
+- throw new Error(`Asset of type ${suggestedAssetMeta.type} not supported`);
+- }
+- }
+- catch (error) {
+- this.failSuggestedAsset(suggestedAssetMeta, error);
+- }
+- const newSuggestedAssets = suggestedAssets.filter(({ id }) => id !== suggestedAssetID);
+- this.update({ suggestedAssets: [...newSuggestedAssets] });
+ });
+ }
+- /**
+- * Rejects a watchAsset request based on its ID by setting its status to "rejected"
+- * and emitting a `:finished` hub event.
+- *
+- * @param suggestedAssetID - The ID of the suggestedAsset to accept.
+- */
+- rejectWatchAsset(suggestedAssetID) {
+- const { suggestedAssets } = this.state;
+- const index = suggestedAssets.findIndex(({ id }) => suggestedAssetID === id);
+- const suggestedAssetMeta = suggestedAssets[index];
+- if (!suggestedAssetMeta) {
+- return;
+- }
+- suggestedAssetMeta.status = SuggestedAssetStatus.rejected;
+- this.hub.emit(`${suggestedAssetMeta.id}:finished`, suggestedAssetMeta);
+- const newSuggestedAssets = suggestedAssets.filter(({ id }) => id !== suggestedAssetID);
+- this.update({ suggestedAssets: [...newSuggestedAssets] });
+- }
+ /**
+ * Takes a new tokens and ignoredTokens array for the current network/account combination
+ * and returns new allTokens and allIgnoredTokens state to update to.
+@@ -553,6 +578,26 @@ class TokensController extends base_controller_1.BaseController {
+ clearIgnoredTokens() {
+ this.update({ ignoredTokens: [], allIgnoredTokens: {} });
+ }
++ // THIS PATCHED METHOD HAS ALREADY BEEN RELEASED IN VERSION 8.0.0 of @metamask/assets-controllers
++ _requestApproval(suggestedAssetMeta) {
++ return __awaiter(this, void 0, void 0, function* () {
++ return this.messagingSystem.call('ApprovalController:addRequest', {
++ id: suggestedAssetMeta.id,
++ origin: 'metamask',
++ type: 'wallet_watchAsset',
++ requestData: {
++ id: suggestedAssetMeta.id,
++ interactingAddress: suggestedAssetMeta.interactingAddress,
++ asset: {
++ address: suggestedAssetMeta.asset.address,
++ decimals: suggestedAssetMeta.asset.decimals,
++ symbol: suggestedAssetMeta.asset.symbol,
++ image: suggestedAssetMeta.asset.image || null,
++ },
++ },
++ }, true);
++ });
++ }
+ }
+ exports.TokensController = TokensController;
+ exports.default = TokensController;
diff --git a/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.ts b/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.ts
index a58b709..20667c0 100644
--- a/node_modules/@metamask/assets-controllers/dist/assetsUtil.d.ts
diff --git a/patches/@metamask+controller-utils+3.0.0.patch b/patches/@metamask+controller-utils+3.0.0.patch
new file mode 100644
index 00000000000..248b0978396
--- /dev/null
+++ b/patches/@metamask+controller-utils+3.0.0.patch
@@ -0,0 +1,64 @@
+diff --git a/node_modules/@metamask/controller-utils/dist/constants.d.ts b/node_modules/@metamask/controller-utils/dist/constants.d.ts
+index 8517c15..e96b439 100644
+--- a/node_modules/@metamask/controller-utils/dist/constants.d.ts
++++ b/node_modules/@metamask/controller-utils/dist/constants.d.ts
+@@ -23,6 +23,7 @@ export declare const ASSET_TYPES: {
+ export declare const TESTNET_TICKER_SYMBOLS: {
+ GOERLI: string;
+ SEPOLIA: string;
++ LINEA_GOERLI: string;
+ };
+ export declare const TESTNET_NETWORK_TYPE_TO_TICKER_SYMBOL: {
+ [K in NetworkType]: string;
+diff --git a/node_modules/@metamask/controller-utils/dist/constants.js b/node_modules/@metamask/controller-utils/dist/constants.js
+index e77af58..0ffbafb 100644
+--- a/node_modules/@metamask/controller-utils/dist/constants.js
++++ b/node_modules/@metamask/controller-utils/dist/constants.js
+@@ -31,12 +31,15 @@ exports.ASSET_TYPES = {
+ exports.TESTNET_TICKER_SYMBOLS = {
+ GOERLI: 'GoerliETH',
+ SEPOLIA: 'SepoliaETH',
++ LINEA_GOERLI: 'LineaETH',
+ };
+ // TYPED NetworkType TICKER SYMBOLS
+ exports.TESTNET_NETWORK_TYPE_TO_TICKER_SYMBOL = {
+ goerli: 'GoerliETH',
+ sepolia: 'SepoliaETH',
++ 'linea-goerli': 'LineaETH',
+ mainnet: '',
++ 'linea-mainnet': '',
+ rpc: '',
+ localhost: '',
+ };
+diff --git a/node_modules/@metamask/controller-utils/dist/types.d.ts b/node_modules/@metamask/controller-utils/dist/types.d.ts
+index 4403885..5f52027 100644
+--- a/node_modules/@metamask/controller-utils/dist/types.d.ts
++++ b/node_modules/@metamask/controller-utils/dist/types.d.ts
+@@ -1,11 +1,13 @@
+ /**
+ * Human-readable network name
+ */
+-export declare type NetworkType = 'localhost' | 'mainnet' | 'goerli' | 'sepolia' | 'rpc';
++export declare type NetworkType = 'localhost' | 'mainnet' | 'goerli' | 'sepolia' | 'linea-goerli' | 'linea-mainnet' | 'rpc';
+ export declare enum NetworksChainId {
+ mainnet = "1",
+ goerli = "5",
+ sepolia = "11155111",
++ "linea-goerli" = "59140",
++ "linea-mainnet" = "59144",
+ localhost = "",
+ rpc = ""
+ }
+diff --git a/node_modules/@metamask/controller-utils/dist/types.js b/node_modules/@metamask/controller-utils/dist/types.js
+index ea81681..a5135ca 100644
+--- a/node_modules/@metamask/controller-utils/dist/types.js
++++ b/node_modules/@metamask/controller-utils/dist/types.js
+@@ -6,6 +6,8 @@ var NetworksChainId;
+ NetworksChainId["mainnet"] = "1";
+ NetworksChainId["goerli"] = "5";
+ NetworksChainId["sepolia"] = "11155111";
++ NetworksChainId["linea-goerli"] = "59140";
++ NetworksChainId["linea-mainnet"] = "59144";
+ NetworksChainId["localhost"] = "";
+ NetworksChainId["rpc"] = "";
+ })(NetworksChainId = exports.NetworksChainId || (exports.NetworksChainId = {}));
diff --git a/patches/@metamask+network-controller+5.0.0.patch b/patches/@metamask+network-controller+5.0.0.patch
new file mode 100644
index 00000000000..d9564df7d77
--- /dev/null
+++ b/patches/@metamask+network-controller+5.0.0.patch
@@ -0,0 +1,22 @@
+diff --git a/node_modules/@metamask/network-controller/dist/NetworkController.js b/node_modules/@metamask/network-controller/dist/NetworkController.js
+index dc2dedb..413e21e 100644
+--- a/node_modules/@metamask/network-controller/dist/NetworkController.js
++++ b/node_modules/@metamask/network-controller/dist/NetworkController.js
+@@ -81,6 +81,8 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
+ case controller_utils_1.MAINNET:
+ case 'goerli':
+ case 'sepolia':
++ case 'linea-goerli':
++ case 'linea-mainnet':
+ this.setupInfuraProvider(type);
+ break;
+ case 'localhost':
+@@ -127,6 +129,8 @@ class NetworkController extends base_controller_1.BaseControllerV2 {
+ return (chainId !== controller_utils_1.NetworksChainId.mainnet &&
+ chainId !== controller_utils_1.NetworksChainId.goerli &&
+ chainId !== controller_utils_1.NetworksChainId.sepolia &&
++ chainId !== controller_utils_1.NetworksChainId["linea-goerli"] &&
++ chainId !== controller_utils_1.NetworksChainId["linea-mainnet"] &&
+ chainId !== controller_utils_1.NetworksChainId.localhost);
+ }
+ setupStandardProvider(rpcTarget, chainId, ticker, nickname) {
diff --git a/patches/@metamask+preferences-controller+2.1.0.patch b/patches/@metamask+preferences-controller+2.1.0.patch
new file mode 100644
index 00000000000..fb4bc4efd4f
--- /dev/null
+++ b/patches/@metamask+preferences-controller+2.1.0.patch
@@ -0,0 +1,27 @@
+diff --git a/node_modules/@metamask/preferences-controller/dist/PreferencesController.js b/node_modules/@metamask/preferences-controller/dist/PreferencesController.js
+index 4ebc51c..74825d5 100644
+--- a/node_modules/@metamask/preferences-controller/dist/PreferencesController.js
++++ b/node_modules/@metamask/preferences-controller/dist/PreferencesController.js
+@@ -29,6 +29,7 @@ class PreferencesController extends base_controller_1.BaseController {
+ useTokenDetection: true,
+ useNftDetection: false,
+ openSeaEnabled: false,
++ isMultiAccountBalancesEnabled: true,
+ disabledRpcMethodPreferences: {
+ eth_sign: false,
+ },
+@@ -251,6 +252,14 @@ class PreferencesController extends base_controller_1.BaseController {
+ const newDisabledRpcMethods = Object.assign(Object.assign({}, disabledRpcMethodPreferences), { [methodName]: isEnabled });
+ this.update({ disabledRpcMethodPreferences: newDisabledRpcMethods });
+ }
++ /**
++ * A setter for the user preferences to enable/disable fetch of multiple accounts balance.
++ *
++ * @param isMultiAccountBalancesEnabled - true to enable multiple accounts balance fetch, false to fetch only selectedAddress.
++ */
++ setIsMultiAccountBalancesEnabled(isMultiAccountBalancesEnabled) {
++ this.update({ isMultiAccountBalancesEnabled });
++ }
+ }
+ exports.PreferencesController = PreferencesController;
+ exports.default = PreferencesController;
diff --git a/patches/@metamask+transaction-controller+4.0.0.patch b/patches/@metamask+transaction-controller+4.0.0.patch
new file mode 100644
index 00000000000..79ac7971f18
--- /dev/null
+++ b/patches/@metamask+transaction-controller+4.0.0.patch
@@ -0,0 +1,246 @@
+diff --git a/node_modules/@metamask/transaction-controller/dist/TransactionController.d.ts b/node_modules/@metamask/transaction-controller/dist/TransactionController.d.ts
+index 6d58813..afa8eb9 100644
+--- a/node_modules/@metamask/transaction-controller/dist/TransactionController.d.ts
++++ b/node_modules/@metamask/transaction-controller/dist/TransactionController.d.ts
+@@ -2,8 +2,9 @@
+ import { EventEmitter } from 'events';
+ import Common from '@ethereumjs/common';
+ import { TypedTransaction } from '@ethereumjs/tx';
+-import { BaseController, BaseConfig, BaseState } from '@metamask/base-controller';
++import { BaseController, BaseConfig, BaseState, RestrictedControllerMessenger } from '@metamask/base-controller';
+ import type { NetworkState, NetworkController } from '@metamask/network-controller';
++import { AcceptRequest as AcceptApprovalRequest, AddApprovalRequest, RejectRequest as RejectApprovalRequest } from '@metamask/approval-controller';
+ /**
+ * @type Result
+ * @property result - Promise resolving to a new transaction hash
+@@ -212,6 +213,18 @@ export declare const CANCEL_RATE = 1.5;
+ * Multiplier used to determine a transaction's increased gas fee during speed up
+ */
+ export declare const SPEED_UP_RATE = 1.1;
++/**
++ * The name of the {@link TransactionController}.
++ */
++declare const controllerName = "TransactionController";
++/**
++ * The external actions available to the {@link TransactionController}.
++ */
++declare type AllowedActions = AddApprovalRequest | AcceptApprovalRequest | RejectApprovalRequest;
++/**
++ * The messenger of the {@link TransactionController}.
++ */
++export declare type TransactionControllerMessenger = RestrictedControllerMessenger;
+ /**
+ * Controller responsible for submitting and managing transactions.
+ */
+@@ -221,6 +234,7 @@ export declare class TransactionController extends BaseController NetworkState;
+ onNetworkStateChange: (listener: (state: NetworkState) => void) => void;
+ getProvider: () => NetworkController['provider'];
++ messenger: TransactionControllerMessenger;
+ }, config?: Partial, state?: Partial);
+ /**
+ * Starts a new polling interval.
+@@ -317,7 +333,7 @@ export declare class TransactionController extends BaseController:finished` hub event.
+ *
+ * @param transactionID - The ID of the transaction to cancel.
+- * @param gasValues - The gas values to use for the cancellation transation.
++ * @param gasValues - The gas values to use for the cancellation transaction.
+ */
+ stopTransaction(transactionID: string, gasValues?: GasPriceValue | FeeMarketEIP1559Values): Promise;
+ /**
+@@ -462,5 +478,9 @@ export declare class TransactionController extends BaseController {
+@@ -122,6 +127,7 @@ class TransactionController extends base_controller_1.BaseController {
+ };
+ this.initialize();
+ const provider = getProvider();
++ this.messagingSystem = messenger;
+ this.getNetworkState = getNetworkState;
+ this.ethQuery = new eth_query_1.default(provider);
+ this.registry = new eth_method_registry_1.default({ provider });
+@@ -240,7 +246,7 @@ class TransactionController extends base_controller_1.BaseController {
+ (0, utils_1.validateTransaction)(transaction);
+ const transactionMeta = {
+ id: (0, uuid_1.v1)(),
+- networkID: network,
++ networkID: network !== null && network !== void 0 ? network : undefined,
+ chainId: providerConfig.chainId,
+ origin,
+ status: TransactionStatus.unapproved,
+@@ -278,6 +284,7 @@ class TransactionController extends base_controller_1.BaseController {
+ transactions.push(transactionMeta);
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
+ this.hub.emit(`unapprovedTransaction`, transactionMeta);
++ this.requestApproval(transactionMeta);
+ return { result, transactionMeta };
+ });
+ }
+@@ -298,7 +305,7 @@ class TransactionController extends base_controller_1.BaseController {
+ */
+ getCommonConfiguration() {
+ const { network: networkId, providerConfig: { type: chain, chainId, nickname: name }, } = this.getNetworkState();
+- if (chain !== controller_utils_1.RPC) {
++ if (chain !== controller_utils_1.RPC && chain !== 'linea-goerli' && chain !== 'linea-mainnet') {
+ return new common_1.default({ chain, hardfork: HARDFORK });
+ }
+ const customChainParams = {
+@@ -330,11 +337,13 @@ class TransactionController extends base_controller_1.BaseController {
+ if (!this.sign) {
+ releaseLock();
+ this.failTransaction(transactionMeta, new Error('No sign method defined.'));
++ this.rejectApproval(transactionMeta);
+ return;
+ }
+ else if (!currentChainId) {
+ releaseLock();
+ this.failTransaction(transactionMeta, new Error('No chainId defined.'));
++ this.rejectApproval(transactionMeta);
+ return;
+ }
+ const chainId = parseInt(currentChainId, undefined);
+@@ -368,9 +377,11 @@ class TransactionController extends base_controller_1.BaseController {
+ transactionMeta.status = TransactionStatus.submitted;
+ this.updateTransaction(transactionMeta);
+ this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
++ this.acceptApproval(transactionMeta);
+ }
+ catch (error) {
+ this.failTransaction(transactionMeta, error);
++ this.rejectApproval(transactionMeta);
+ }
+ finally {
+ releaseLock();
+@@ -392,13 +403,14 @@ class TransactionController extends base_controller_1.BaseController {
+ this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
+ const transactions = this.state.transactions.filter(({ id }) => id !== transactionID);
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
++ this.rejectApproval(transactionMeta);
+ }
+ /**
+ * Attempts to cancel a transaction based on its ID by setting its status to "rejected"
+ * and emitting a `:finished` hub event.
+ *
+ * @param transactionID - The ID of the transaction to cancel.
+- * @param gasValues - The gas values to use for the cancellation transation.
++ * @param gasValues - The gas values to use for the cancellation transaction.
+ */
+ stopTransaction(transactionID, gasValues) {
+ var _a, _b;
+@@ -458,6 +470,7 @@ class TransactionController extends base_controller_1.BaseController {
+ yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTransaction]);
+ transactionMeta.status = TransactionStatus.cancelled;
+ this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
++ this.rejectApproval(transactionMeta);
+ });
+ }
+ /**
+@@ -921,6 +934,46 @@ class TransactionController extends base_controller_1.BaseController {
+ isGasDataOutdated(remoteGasUsed, localGasUsed) {
+ return remoteGasUsed !== localGasUsed;
+ }
++ requestApproval(txMeta, { shouldShowRequest } = { shouldShowRequest: true }) {
++ return __awaiter(this, void 0, void 0, function* () {
++ const id = this.getApprovalId(txMeta);
++ const { origin } = txMeta;
++ const type = 'transaction';
++ const requestData = { txId: txMeta.id };
++ try {
++ yield this.messagingSystem.call('ApprovalController:addRequest', {
++ id,
++ origin: origin || 'metamask',
++ type,
++ requestData,
++ }, shouldShowRequest);
++ }
++ catch (error) {
++ console.info('Failed to request transaction approval', error);
++ }
++ });
++ }
++ acceptApproval(txMeta) {
++ const id = this.getApprovalId(txMeta);
++ try {
++ this.messagingSystem.call('ApprovalController:acceptRequest', id);
++ }
++ catch (error) {
++ console.info('Failed to accept transaction approval request', error);
++ }
++ }
++ rejectApproval(txMeta) {
++ const id = this.getApprovalId(txMeta);
++ try {
++ this.messagingSystem.call('ApprovalController:rejectRequest', id, new Error('Rejected'));
++ }
++ catch (error) {
++ console.info('Failed to reject transaction approval request', error);
++ }
++ }
++ getApprovalId(txMeta) {
++ return String(txMeta.id);
++ }
+ }
+ exports.TransactionController = TransactionController;
+ exports.default = TransactionController;
+diff --git a/node_modules/@metamask/transaction-controller/dist/TransactionController.js.map b/node_modules/@metamask/transaction-controller/dist/TransactionController.js.map
+index 9c3e205..d4f4b1d 100644
+--- a/node_modules/@metamask/transaction-controller/dist/TransactionController.js.map
++++ b/node_modules/@metamask/transaction-controller/dist/TransactionController.js.map
+@@ -1 +1 @@
+-{"version":3,"file":"TransactionController.js","sourceRoot":"","sources":["../src/TransactionController.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from 'events';\nimport { addHexPrefix, bufferToHex, BN } from 'ethereumjs-util';\nimport { ethErrors } from 'eth-rpc-errors';\nimport MethodRegistry from 'eth-method-registry';\nimport EthQuery from 'eth-query';\nimport Common from '@ethereumjs/common';\nimport { TransactionFactory, TypedTransaction } from '@ethereumjs/tx';\nimport { v1 as random } from 'uuid';\nimport { Mutex } from 'async-mutex';\nimport {\n BaseController,\n BaseConfig,\n BaseState,\n} from '@metamask/base-controller';\nimport type {\n NetworkState,\n NetworkController,\n} from '@metamask/network-controller';\nimport {\n BNToHex,\n fractionBN,\n hexToBN,\n safelyExecute,\n isSmartContractCode,\n query,\n MAINNET,\n RPC,\n} from '@metamask/controller-utils';\nimport {\n normalizeTransaction,\n validateTransaction,\n handleTransactionFetch,\n getIncreasedPriceFromExisting,\n isEIP1559Transaction,\n isGasPriceValue,\n isFeeMarketEIP1559Values,\n validateGasValues,\n validateMinimumIncrease,\n ESTIMATE_GAS_ERROR,\n} from './utils';\n\nconst HARDFORK = 'london';\n\n/**\n * @type Result\n * @property result - Promise resolving to a new transaction hash\n * @property transactionMeta - Meta information about this new transaction\n */\nexport interface Result {\n result: Promise;\n transactionMeta: TransactionMeta;\n}\n\n/**\n * @type Fetch All Options\n * @property fromBlock - String containing a specific block decimal number\n * @property etherscanApiKey - API key to be used to fetch token transactions\n */\nexport interface FetchAllOptions {\n fromBlock?: string;\n etherscanApiKey?: string;\n}\n\n/**\n * @type Transaction\n *\n * Transaction representation\n * @property chainId - Network ID as per EIP-155\n * @property data - Data to pass with this transaction\n * @property from - Address to send this transaction from\n * @property gas - Gas to send with this transaction\n * @property gasPrice - Price of gas with this transaction\n * @property gasUsed - Gas used in the transaction\n * @property nonce - Unique number to prevent replay attacks\n * @property to - Address to send this transaction to\n * @property value - Value associated with this transaction\n */\nexport interface Transaction {\n chainId?: number;\n data?: string;\n from: string;\n gas?: string;\n gasPrice?: string;\n gasUsed?: string;\n nonce?: string;\n to?: string;\n value?: string;\n maxFeePerGas?: string;\n maxPriorityFeePerGas?: string;\n estimatedBaseFee?: string;\n estimateGasError?: string;\n}\n\nexport interface GasPriceValue {\n gasPrice: string;\n}\n\nexport interface FeeMarketEIP1559Values {\n maxFeePerGas: string;\n maxPriorityFeePerGas: string;\n}\n\n/**\n * The status of the transaction. Each status represents the state of the transaction internally\n * in the wallet. Some of these correspond with the state of the transaction on the network, but\n * some are wallet-specific.\n */\nexport enum TransactionStatus {\n approved = 'approved',\n cancelled = 'cancelled',\n confirmed = 'confirmed',\n failed = 'failed',\n rejected = 'rejected',\n signed = 'signed',\n submitted = 'submitted',\n unapproved = 'unapproved',\n}\n\n/**\n * Options for wallet device.\n */\nexport enum WalletDevice {\n MM_MOBILE = 'metamask_mobile',\n MM_EXTENSION = 'metamask_extension',\n OTHER = 'other_device',\n}\n\ntype TransactionMetaBase = {\n isTransfer?: boolean;\n transferInformation?: {\n symbol: string;\n contractAddress: string;\n decimals: number;\n };\n id: string;\n networkID?: string;\n chainId?: string;\n origin?: string;\n rawTransaction?: string;\n time: number;\n toSmartContract?: boolean;\n transaction: Transaction;\n transactionHash?: string;\n blockNumber?: string;\n deviceConfirmedOn?: WalletDevice;\n verifiedOnBlockchain?: boolean;\n};\n\n/**\n * @type TransactionMeta\n *\n * TransactionMeta representation\n * @property error - Synthesized error information for failed transactions\n * @property id - Generated UUID associated with this transaction\n * @property networkID - Network code as per EIP-155 for this transaction\n * @property origin - Origin this transaction was sent from\n * @property deviceConfirmedOn - string to indicate what device the transaction was confirmed\n * @property rawTransaction - Hex representation of the underlying transaction\n * @property status - String status of this transaction\n * @property time - Timestamp associated with this transaction\n * @property toSmartContract - Whether transaction recipient is a smart contract\n * @property transaction - Underlying Transaction object\n * @property transactionHash - Hash of a successful transaction\n * @property blockNumber - Number of the block where the transaction has been included\n */\nexport type TransactionMeta =\n | ({\n status: Exclude;\n } & TransactionMetaBase)\n | ({ status: TransactionStatus.failed; error: Error } & TransactionMetaBase);\n\n/**\n * @type EtherscanTransactionMeta\n *\n * EtherscanTransactionMeta representation\n * @property blockNumber - Number of the block where the transaction has been included\n * @property timeStamp - Timestamp associated with this transaction\n * @property hash - Hash of a successful transaction\n * @property nonce - Nonce of the transaction\n * @property blockHash - Hash of the block where the transaction has been included\n * @property transactionIndex - Etherscan internal index for this transaction\n * @property from - Address to send this transaction from\n * @property to - Address to send this transaction to\n * @property gas - Gas to send with this transaction\n * @property gasPrice - Price of gas with this transaction\n * @property isError - Synthesized error information for failed transactions\n * @property txreceipt_status - Receipt status for this transaction\n * @property input - input of the transaction\n * @property contractAddress - Address of the contract\n * @property cumulativeGasUsed - Amount of gas used\n * @property confirmations - Number of confirmations\n */\nexport interface EtherscanTransactionMeta {\n blockNumber: string;\n timeStamp: string;\n hash: string;\n nonce: string;\n blockHash: string;\n transactionIndex: string;\n from: string;\n to: string;\n value: string;\n gas: string;\n gasPrice: string;\n cumulativeGasUsed: string;\n gasUsed: string;\n isError: string;\n txreceipt_status: string;\n input: string;\n contractAddress: string;\n confirmations: string;\n tokenDecimal: string;\n tokenSymbol: string;\n}\n\n/**\n * @type TransactionConfig\n *\n * Transaction controller configuration\n * @property interval - Polling interval used to fetch new currency rate\n * @property provider - Provider used to create a new underlying EthQuery instance\n * @property sign - Method used to sign transactions\n */\nexport interface TransactionConfig extends BaseConfig {\n interval: number;\n sign?: (transaction: Transaction, from: string) => Promise;\n txHistoryLimit: number;\n}\n\n/**\n * @type MethodData\n *\n * Method data registry object\n * @property registryMethod - Registry method raw string\n * @property parsedRegistryMethod - Registry method object, containing name and method arguments\n */\nexport interface MethodData {\n registryMethod: string;\n parsedRegistryMethod: Record;\n}\n\n/**\n * @type TransactionState\n *\n * Transaction controller state\n * @property transactions - A list of TransactionMeta objects\n * @property methodData - Object containing all known method data information\n */\nexport interface TransactionState extends BaseState {\n transactions: TransactionMeta[];\n methodData: { [key: string]: MethodData };\n}\n\n/**\n * Multiplier used to determine a transaction's increased gas fee during cancellation\n */\nexport const CANCEL_RATE = 1.5;\n\n/**\n * Multiplier used to determine a transaction's increased gas fee during speed up\n */\nexport const SPEED_UP_RATE = 1.1;\n\n/**\n * Controller responsible for submitting and managing transactions.\n */\nexport class TransactionController extends BaseController<\n TransactionConfig,\n TransactionState\n> {\n private ethQuery: any;\n\n private registry: any;\n\n private handle?: ReturnType;\n\n private mutex = new Mutex();\n\n private getNetworkState: () => NetworkState;\n\n private failTransaction(transactionMeta: TransactionMeta, error: Error) {\n const newTransactionMeta = {\n ...transactionMeta,\n error,\n status: TransactionStatus.failed,\n };\n this.updateTransaction(newTransactionMeta);\n this.hub.emit(`${transactionMeta.id}:finished`, newTransactionMeta);\n }\n\n private async registryLookup(fourBytePrefix: string): Promise {\n const registryMethod = await this.registry.lookup(fourBytePrefix);\n const parsedRegistryMethod = this.registry.parse(registryMethod);\n return { registryMethod, parsedRegistryMethod };\n }\n\n /**\n * Normalizes the transaction information from etherscan\n * to be compatible with the TransactionMeta interface.\n *\n * @param txMeta - The transaction.\n * @param currentNetworkID - The current network ID.\n * @param currentChainId - The current chain ID.\n * @returns The normalized transaction.\n */\n private normalizeTx(\n txMeta: EtherscanTransactionMeta,\n currentNetworkID: string,\n currentChainId: string,\n ): TransactionMeta {\n const time = parseInt(txMeta.timeStamp, 10) * 1000;\n const normalizedTransactionBase = {\n blockNumber: txMeta.blockNumber,\n id: random({ msecs: time }),\n networkID: currentNetworkID,\n chainId: currentChainId,\n time,\n transaction: {\n data: txMeta.input,\n from: txMeta.from,\n gas: BNToHex(new BN(txMeta.gas)),\n gasPrice: BNToHex(new BN(txMeta.gasPrice)),\n gasUsed: BNToHex(new BN(txMeta.gasUsed)),\n nonce: BNToHex(new BN(txMeta.nonce)),\n to: txMeta.to,\n value: BNToHex(new BN(txMeta.value)),\n },\n transactionHash: txMeta.hash,\n verifiedOnBlockchain: false,\n };\n\n /* istanbul ignore else */\n if (txMeta.isError === '0') {\n return {\n ...normalizedTransactionBase,\n status: TransactionStatus.confirmed,\n };\n }\n\n /* istanbul ignore next */\n return {\n ...normalizedTransactionBase,\n error: new Error('Transaction failed'),\n status: TransactionStatus.failed,\n };\n }\n\n private normalizeTokenTx = (\n txMeta: EtherscanTransactionMeta,\n currentNetworkID: string,\n currentChainId: string,\n ): TransactionMeta => {\n const time = parseInt(txMeta.timeStamp, 10) * 1000;\n const {\n to,\n from,\n gas,\n gasPrice,\n gasUsed,\n hash,\n contractAddress,\n tokenDecimal,\n tokenSymbol,\n value,\n } = txMeta;\n return {\n id: random({ msecs: time }),\n isTransfer: true,\n networkID: currentNetworkID,\n chainId: currentChainId,\n status: TransactionStatus.confirmed,\n time,\n transaction: {\n chainId: 1,\n from,\n gas,\n gasPrice,\n gasUsed,\n to,\n value,\n },\n transactionHash: hash,\n transferInformation: {\n contractAddress,\n decimals: Number(tokenDecimal),\n symbol: tokenSymbol,\n },\n verifiedOnBlockchain: false,\n };\n };\n\n /**\n * EventEmitter instance used to listen to specific transactional events\n */\n hub = new EventEmitter();\n\n /**\n * Name of this controller used during composition\n */\n override name = 'TransactionController';\n\n /**\n * Method used to sign transactions\n */\n sign?: (\n transaction: TypedTransaction,\n from: string,\n ) => Promise;\n\n /**\n * Creates a TransactionController instance.\n *\n * @param options - The controller options.\n * @param options.getNetworkState - Gets the state of the network controller.\n * @param options.onNetworkStateChange - Allows subscribing to network controller state changes.\n * @param options.getProvider - Returns a provider for the current network.\n * @param config - Initial options used to configure this controller.\n * @param state - Initial state to set on this controller.\n */\n constructor(\n {\n getNetworkState,\n onNetworkStateChange,\n getProvider,\n }: {\n getNetworkState: () => NetworkState;\n onNetworkStateChange: (listener: (state: NetworkState) => void) => void;\n getProvider: () => NetworkController['provider'];\n },\n config?: Partial,\n state?: Partial,\n ) {\n super(config, state);\n this.defaultConfig = {\n interval: 15000,\n txHistoryLimit: 40,\n };\n\n this.defaultState = {\n methodData: {},\n transactions: [],\n };\n this.initialize();\n const provider = getProvider();\n this.getNetworkState = getNetworkState;\n this.ethQuery = new EthQuery(provider);\n this.registry = new MethodRegistry({ provider });\n onNetworkStateChange(() => {\n const newProvider = getProvider();\n this.ethQuery = new EthQuery(newProvider);\n this.registry = new MethodRegistry({ provider: newProvider });\n });\n this.poll();\n }\n\n /**\n * Starts a new polling interval.\n *\n * @param interval - The polling interval used to fetch new transaction statuses.\n */\n async poll(interval?: number): Promise {\n interval && this.configure({ interval }, false, false);\n this.handle && clearTimeout(this.handle);\n await safelyExecute(() => this.queryTransactionStatuses());\n this.handle = setTimeout(() => {\n this.poll(this.config.interval);\n }, this.config.interval);\n }\n\n /**\n * Handle new method data request.\n *\n * @param fourBytePrefix - The method prefix.\n * @returns The method data object corresponding to the given signature prefix.\n */\n async handleMethodData(fourBytePrefix: string): Promise {\n const releaseLock = await this.mutex.acquire();\n try {\n const { methodData } = this.state;\n const knownMethod = Object.keys(methodData).find(\n (knownFourBytePrefix) => fourBytePrefix === knownFourBytePrefix,\n );\n if (knownMethod) {\n return methodData[fourBytePrefix];\n }\n const registry = await this.registryLookup(fourBytePrefix);\n this.update({\n methodData: { ...methodData, ...{ [fourBytePrefix]: registry } },\n });\n return registry;\n } finally {\n releaseLock();\n }\n }\n\n /**\n * Add a new unapproved transaction to state. Parameters will be validated, a\n * unique transaction id will be generated, and gas and gasPrice will be calculated\n * if not provided. If A `:unapproved` hub event will be emitted once added.\n *\n * @param transaction - The transaction object to add.\n * @param origin - The domain origin to append to the generated TransactionMeta.\n * @param deviceConfirmedOn - An enum to indicate what device the transaction was confirmed to append to the generated TransactionMeta.\n * @returns Object containing a promise resolving to the transaction hash if approved.\n */\n async addTransaction(\n transaction: Transaction,\n origin?: string,\n deviceConfirmedOn?: WalletDevice,\n ): Promise {\n const { providerConfig, network } = this.getNetworkState();\n const { transactions } = this.state;\n transaction = normalizeTransaction(transaction);\n validateTransaction(transaction);\n\n const transactionMeta: TransactionMeta = {\n id: random(),\n networkID: network,\n chainId: providerConfig.chainId,\n origin,\n status: TransactionStatus.unapproved as TransactionStatus.unapproved,\n time: Date.now(),\n transaction,\n deviceConfirmedOn,\n verifiedOnBlockchain: false,\n };\n\n try {\n const { gas, estimateGasError } = await this.estimateGas(transaction);\n transaction.gas = gas;\n transaction.estimateGasError = estimateGasError;\n } catch (error: any) {\n this.failTransaction(transactionMeta, error);\n return Promise.reject(error);\n }\n\n const result: Promise = new Promise((resolve, reject) => {\n this.hub.once(\n `${transactionMeta.id}:finished`,\n (meta: TransactionMeta) => {\n switch (meta.status) {\n case TransactionStatus.submitted:\n return resolve(meta.transactionHash as string);\n case TransactionStatus.rejected:\n return reject(\n ethErrors.provider.userRejectedRequest(\n 'User rejected the transaction',\n ),\n );\n case TransactionStatus.cancelled:\n return reject(\n ethErrors.rpc.internal('User cancelled the transaction'),\n );\n case TransactionStatus.failed:\n return reject(ethErrors.rpc.internal(meta.error.message));\n /* istanbul ignore next */\n default:\n return reject(\n ethErrors.rpc.internal(\n `MetaMask Tx Signature: Unknown problem: ${JSON.stringify(\n meta,\n )}`,\n ),\n );\n }\n },\n );\n });\n\n transactions.push(transactionMeta);\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n this.hub.emit(`unapprovedTransaction`, transactionMeta);\n return { result, transactionMeta };\n }\n\n prepareUnsignedEthTx(txParams: Record): TypedTransaction {\n return TransactionFactory.fromTxData(txParams, {\n common: this.getCommonConfiguration(),\n freeze: false,\n });\n }\n\n /**\n * `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for\n * specifying which chain, network, hardfork and EIPs to support for\n * a transaction. By referencing this configuration, and analyzing the fields\n * specified in txParams, @ethereumjs/tx is able to determine which EIP-2718\n * transaction type to use.\n *\n * @returns {Common} common configuration object\n */\n\n getCommonConfiguration(): Common {\n const {\n network: networkId,\n providerConfig: { type: chain, chainId, nickname: name },\n } = this.getNetworkState();\n\n if (chain !== RPC) {\n return new Common({ chain, hardfork: HARDFORK });\n }\n\n const customChainParams = {\n name,\n chainId: parseInt(chainId, undefined),\n networkId: parseInt(networkId, undefined),\n };\n\n return Common.forCustomChain(MAINNET, customChainParams, HARDFORK);\n }\n\n /**\n * Approves a transaction and updates it's status in state. If this is not a\n * retry transaction, a nonce will be generated. The transaction is signed\n * using the sign configuration property, then published to the blockchain.\n * A `:finished` hub event is fired after success or failure.\n *\n * @param transactionID - The ID of the transaction to approve.\n */\n async approveTransaction(transactionID: string) {\n const { transactions } = this.state;\n const releaseLock = await this.mutex.acquire();\n const { providerConfig } = this.getNetworkState();\n const { chainId: currentChainId } = providerConfig;\n const index = transactions.findIndex(({ id }) => transactionID === id);\n const transactionMeta = transactions[index];\n const { nonce } = transactionMeta.transaction;\n\n try {\n const { from } = transactionMeta.transaction;\n if (!this.sign) {\n releaseLock();\n this.failTransaction(\n transactionMeta,\n new Error('No sign method defined.'),\n );\n return;\n } else if (!currentChainId) {\n releaseLock();\n this.failTransaction(transactionMeta, new Error('No chainId defined.'));\n return;\n }\n\n const chainId = parseInt(currentChainId, undefined);\n const { approved: status } = TransactionStatus;\n\n const txNonce =\n nonce ||\n (await query(this.ethQuery, 'getTransactionCount', [from, 'pending']));\n\n transactionMeta.status = status;\n transactionMeta.transaction.nonce = txNonce;\n transactionMeta.transaction.chainId = chainId;\n\n const baseTxParams = {\n ...transactionMeta.transaction,\n gasLimit: transactionMeta.transaction.gas,\n chainId,\n nonce: txNonce,\n status,\n };\n\n const isEIP1559 = isEIP1559Transaction(transactionMeta.transaction);\n\n const txParams = isEIP1559\n ? {\n ...baseTxParams,\n maxFeePerGas: transactionMeta.transaction.maxFeePerGas,\n maxPriorityFeePerGas:\n transactionMeta.transaction.maxPriorityFeePerGas,\n estimatedBaseFee: transactionMeta.transaction.estimatedBaseFee,\n // specify type 2 if maxFeePerGas and maxPriorityFeePerGas are set\n type: 2,\n }\n : baseTxParams;\n\n // delete gasPrice if maxFeePerGas and maxPriorityFeePerGas are set\n if (isEIP1559) {\n delete txParams.gasPrice;\n }\n\n const unsignedEthTx = this.prepareUnsignedEthTx(txParams);\n const signedTx = await this.sign(unsignedEthTx, from);\n transactionMeta.status = TransactionStatus.signed;\n this.updateTransaction(transactionMeta);\n const rawTransaction = bufferToHex(signedTx.serialize());\n\n transactionMeta.rawTransaction = rawTransaction;\n this.updateTransaction(transactionMeta);\n const transactionHash = await query(this.ethQuery, 'sendRawTransaction', [\n rawTransaction,\n ]);\n transactionMeta.transactionHash = transactionHash;\n transactionMeta.status = TransactionStatus.submitted;\n this.updateTransaction(transactionMeta);\n this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);\n } catch (error: any) {\n this.failTransaction(transactionMeta, error);\n } finally {\n releaseLock();\n }\n }\n\n /**\n * Cancels a transaction based on its ID by setting its status to \"rejected\"\n * and emitting a `:finished` hub event.\n *\n * @param transactionID - The ID of the transaction to cancel.\n */\n cancelTransaction(transactionID: string) {\n const transactionMeta = this.state.transactions.find(\n ({ id }) => id === transactionID,\n );\n if (!transactionMeta) {\n return;\n }\n transactionMeta.status = TransactionStatus.rejected;\n this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);\n const transactions = this.state.transactions.filter(\n ({ id }) => id !== transactionID,\n );\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n }\n\n /**\n * Attempts to cancel a transaction based on its ID by setting its status to \"rejected\"\n * and emitting a `:finished` hub event.\n *\n * @param transactionID - The ID of the transaction to cancel.\n * @param gasValues - The gas values to use for the cancellation transation.\n */\n async stopTransaction(\n transactionID: string,\n gasValues?: GasPriceValue | FeeMarketEIP1559Values,\n ) {\n if (gasValues) {\n validateGasValues(gasValues);\n }\n const transactionMeta = this.state.transactions.find(\n ({ id }) => id === transactionID,\n );\n if (!transactionMeta) {\n return;\n }\n\n if (!this.sign) {\n throw new Error('No sign method defined.');\n }\n\n // gasPrice (legacy non EIP1559)\n const minGasPrice = getIncreasedPriceFromExisting(\n transactionMeta.transaction.gasPrice,\n CANCEL_RATE,\n );\n\n const gasPriceFromValues = isGasPriceValue(gasValues) && gasValues.gasPrice;\n\n const newGasPrice =\n (gasPriceFromValues &&\n validateMinimumIncrease(gasPriceFromValues, minGasPrice)) ||\n minGasPrice;\n\n // maxFeePerGas (EIP1559)\n const existingMaxFeePerGas = transactionMeta.transaction?.maxFeePerGas;\n const minMaxFeePerGas = getIncreasedPriceFromExisting(\n existingMaxFeePerGas,\n CANCEL_RATE,\n );\n const maxFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxFeePerGas;\n const newMaxFeePerGas =\n (maxFeePerGasValues &&\n validateMinimumIncrease(maxFeePerGasValues, minMaxFeePerGas)) ||\n (existingMaxFeePerGas && minMaxFeePerGas);\n\n // maxPriorityFeePerGas (EIP1559)\n const existingMaxPriorityFeePerGas =\n transactionMeta.transaction?.maxPriorityFeePerGas;\n const minMaxPriorityFeePerGas = getIncreasedPriceFromExisting(\n existingMaxPriorityFeePerGas,\n CANCEL_RATE,\n );\n const maxPriorityFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxPriorityFeePerGas;\n const newMaxPriorityFeePerGas =\n (maxPriorityFeePerGasValues &&\n validateMinimumIncrease(\n maxPriorityFeePerGasValues,\n minMaxPriorityFeePerGas,\n )) ||\n (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);\n\n const txParams =\n newMaxFeePerGas && newMaxPriorityFeePerGas\n ? {\n from: transactionMeta.transaction.from,\n gasLimit: transactionMeta.transaction.gas,\n maxFeePerGas: newMaxFeePerGas,\n maxPriorityFeePerGas: newMaxPriorityFeePerGas,\n type: 2,\n nonce: transactionMeta.transaction.nonce,\n to: transactionMeta.transaction.from,\n value: '0x0',\n }\n : {\n from: transactionMeta.transaction.from,\n gasLimit: transactionMeta.transaction.gas,\n gasPrice: newGasPrice,\n nonce: transactionMeta.transaction.nonce,\n to: transactionMeta.transaction.from,\n value: '0x0',\n };\n\n const unsignedEthTx = this.prepareUnsignedEthTx(txParams);\n\n const signedTx = await this.sign(\n unsignedEthTx,\n transactionMeta.transaction.from,\n );\n const rawTransaction = bufferToHex(signedTx.serialize());\n await query(this.ethQuery, 'sendRawTransaction', [rawTransaction]);\n transactionMeta.status = TransactionStatus.cancelled;\n this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);\n }\n\n /**\n * Attempts to speed up a transaction increasing transaction gasPrice by ten percent.\n *\n * @param transactionID - The ID of the transaction to speed up.\n * @param gasValues - The gas values to use for the speed up transation.\n */\n async speedUpTransaction(\n transactionID: string,\n gasValues?: GasPriceValue | FeeMarketEIP1559Values,\n ) {\n if (gasValues) {\n validateGasValues(gasValues);\n }\n const transactionMeta = this.state.transactions.find(\n ({ id }) => id === transactionID,\n );\n /* istanbul ignore next */\n if (!transactionMeta) {\n return;\n }\n\n /* istanbul ignore next */\n if (!this.sign) {\n throw new Error('No sign method defined.');\n }\n\n const { transactions } = this.state;\n\n // gasPrice (legacy non EIP1559)\n const minGasPrice = getIncreasedPriceFromExisting(\n transactionMeta.transaction.gasPrice,\n SPEED_UP_RATE,\n );\n\n const gasPriceFromValues = isGasPriceValue(gasValues) && gasValues.gasPrice;\n\n const newGasPrice =\n (gasPriceFromValues &&\n validateMinimumIncrease(gasPriceFromValues, minGasPrice)) ||\n minGasPrice;\n\n // maxFeePerGas (EIP1559)\n const existingMaxFeePerGas = transactionMeta.transaction?.maxFeePerGas;\n const minMaxFeePerGas = getIncreasedPriceFromExisting(\n existingMaxFeePerGas,\n SPEED_UP_RATE,\n );\n const maxFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxFeePerGas;\n const newMaxFeePerGas =\n (maxFeePerGasValues &&\n validateMinimumIncrease(maxFeePerGasValues, minMaxFeePerGas)) ||\n (existingMaxFeePerGas && minMaxFeePerGas);\n\n // maxPriorityFeePerGas (EIP1559)\n const existingMaxPriorityFeePerGas =\n transactionMeta.transaction?.maxPriorityFeePerGas;\n const minMaxPriorityFeePerGas = getIncreasedPriceFromExisting(\n existingMaxPriorityFeePerGas,\n SPEED_UP_RATE,\n );\n const maxPriorityFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxPriorityFeePerGas;\n const newMaxPriorityFeePerGas =\n (maxPriorityFeePerGasValues &&\n validateMinimumIncrease(\n maxPriorityFeePerGasValues,\n minMaxPriorityFeePerGas,\n )) ||\n (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);\n\n const txParams =\n newMaxFeePerGas && newMaxPriorityFeePerGas\n ? {\n ...transactionMeta.transaction,\n gasLimit: transactionMeta.transaction.gas,\n maxFeePerGas: newMaxFeePerGas,\n maxPriorityFeePerGas: newMaxPriorityFeePerGas,\n type: 2,\n }\n : {\n ...transactionMeta.transaction,\n gasLimit: transactionMeta.transaction.gas,\n gasPrice: newGasPrice,\n };\n\n const unsignedEthTx = this.prepareUnsignedEthTx(txParams);\n\n const signedTx = await this.sign(\n unsignedEthTx,\n transactionMeta.transaction.from,\n );\n const rawTransaction = bufferToHex(signedTx.serialize());\n const transactionHash = await query(this.ethQuery, 'sendRawTransaction', [\n rawTransaction,\n ]);\n const baseTransactionMeta = {\n ...transactionMeta,\n id: random(),\n time: Date.now(),\n transactionHash,\n };\n const newTransactionMeta =\n newMaxFeePerGas && newMaxPriorityFeePerGas\n ? {\n ...baseTransactionMeta,\n transaction: {\n ...transactionMeta.transaction,\n maxFeePerGas: newMaxFeePerGas,\n maxPriorityFeePerGas: newMaxPriorityFeePerGas,\n },\n }\n : {\n ...baseTransactionMeta,\n transaction: {\n ...transactionMeta.transaction,\n gasPrice: newGasPrice,\n },\n };\n transactions.push(newTransactionMeta);\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta);\n }\n\n /**\n * Estimates required gas for a given transaction.\n *\n * @param transaction - The transaction to estimate gas for.\n * @returns The gas and gas price.\n */\n async estimateGas(transaction: Transaction) {\n const estimatedTransaction = { ...transaction };\n const {\n gas,\n gasPrice: providedGasPrice,\n to,\n value,\n data,\n } = estimatedTransaction;\n const gasPrice =\n typeof providedGasPrice === 'undefined'\n ? await query(this.ethQuery, 'gasPrice')\n : providedGasPrice;\n const { isCustomNetwork } = this.getNetworkState();\n // 1. If gas is already defined on the transaction, use it\n if (typeof gas !== 'undefined') {\n return { gas, gasPrice };\n }\n const { gasLimit } = await query(this.ethQuery, 'getBlockByNumber', [\n 'latest',\n false,\n ]);\n\n // 2. If to is not defined or this is not a contract address, and there is no data use 0x5208 / 21000.\n // If the newtwork is a custom network then bypass this check and fetch 'estimateGas'.\n /* istanbul ignore next */\n const code = to ? await query(this.ethQuery, 'getCode', [to]) : undefined;\n /* istanbul ignore next */\n if (\n !isCustomNetwork &&\n (!to || (to && !data && (!code || code === '0x')))\n ) {\n return { gas: '0x5208', gasPrice };\n }\n\n // if data, should be hex string format\n estimatedTransaction.data = !data\n ? data\n : /* istanbul ignore next */ addHexPrefix(data);\n\n // 3. If this is a contract address, safely estimate gas using RPC\n estimatedTransaction.value =\n typeof value === 'undefined' ? '0x0' : /* istanbul ignore next */ value;\n const gasLimitBN = hexToBN(gasLimit);\n estimatedTransaction.gas = BNToHex(fractionBN(gasLimitBN, 19, 20));\n\n let gasHex;\n let estimateGasError;\n try {\n gasHex = await query(this.ethQuery, 'estimateGas', [\n estimatedTransaction,\n ]);\n } catch (error) {\n estimateGasError = ESTIMATE_GAS_ERROR;\n }\n // 4. Pad estimated gas without exceeding the most recent block gasLimit. If the network is a\n // a custom network then return the eth_estimateGas value.\n const gasBN = hexToBN(gasHex);\n const maxGasBN = gasLimitBN.muln(0.9);\n const paddedGasBN = gasBN.muln(1.5);\n /* istanbul ignore next */\n if (gasBN.gt(maxGasBN) || isCustomNetwork) {\n return { gas: addHexPrefix(gasHex), gasPrice, estimateGasError };\n }\n\n /* istanbul ignore next */\n if (paddedGasBN.lt(maxGasBN)) {\n return {\n gas: addHexPrefix(BNToHex(paddedGasBN)),\n gasPrice,\n estimateGasError,\n };\n }\n return { gas: addHexPrefix(BNToHex(maxGasBN)), gasPrice };\n }\n\n /**\n * Check the status of submitted transactions on the network to determine whether they have\n * been included in a block. Any that have been included in a block are marked as confirmed.\n */\n async queryTransactionStatuses() {\n const { transactions } = this.state;\n const { providerConfig, network: currentNetworkID } =\n this.getNetworkState();\n const { chainId: currentChainId } = providerConfig;\n let gotUpdates = false;\n await safelyExecute(() =>\n Promise.all(\n transactions.map(async (meta, index) => {\n // Using fallback to networkID only when there is no chainId present.\n // Should be removed when networkID is completely removed.\n const txBelongsToCurrentChain =\n meta.chainId === currentChainId ||\n (!meta.chainId && meta.networkID === currentNetworkID);\n\n if (!meta.verifiedOnBlockchain && txBelongsToCurrentChain) {\n const [reconciledTx, updateRequired] =\n await this.blockchainTransactionStateReconciler(meta);\n if (updateRequired) {\n transactions[index] = reconciledTx;\n gotUpdates = updateRequired;\n }\n }\n }),\n ),\n );\n\n /* istanbul ignore else */\n if (gotUpdates) {\n this.update({\n transactions: this.trimTransactionsForState(transactions),\n });\n }\n }\n\n /**\n * Updates an existing transaction in state.\n *\n * @param transactionMeta - The new transaction to store in state.\n */\n updateTransaction(transactionMeta: TransactionMeta) {\n const { transactions } = this.state;\n transactionMeta.transaction = normalizeTransaction(\n transactionMeta.transaction,\n );\n validateTransaction(transactionMeta.transaction);\n const index = transactions.findIndex(({ id }) => transactionMeta.id === id);\n transactions[index] = transactionMeta;\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n }\n\n /**\n * Removes all transactions from state, optionally based on the current network.\n *\n * @param ignoreNetwork - Determines whether to wipe all transactions, or just those on the\n * current network. If `true`, all transactions are wiped.\n */\n wipeTransactions(ignoreNetwork?: boolean) {\n /* istanbul ignore next */\n if (ignoreNetwork) {\n this.update({ transactions: [] });\n return;\n }\n const { providerConfig, network: currentNetworkID } =\n this.getNetworkState();\n const { chainId: currentChainId } = providerConfig;\n const newTransactions = this.state.transactions.filter(\n ({ networkID, chainId }) => {\n // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.\n const isCurrentNetwork =\n chainId === currentChainId ||\n (!chainId && networkID === currentNetworkID);\n return !isCurrentNetwork;\n },\n );\n\n this.update({\n transactions: this.trimTransactionsForState(newTransactions),\n });\n }\n\n /**\n * Get transactions from Etherscan for the given address. By default all transactions are\n * returned, but the `fromBlock` option can be given to filter just for transactions from a\n * specific block onward.\n *\n * @param address - The address to fetch the transactions for.\n * @param opt - Object containing optional data, fromBlock and Etherscan API key.\n * @returns The block number of the latest incoming transaction.\n */\n async fetchAll(\n address: string,\n opt?: FetchAllOptions,\n ): Promise {\n const { providerConfig, network: currentNetworkID } =\n this.getNetworkState();\n const { chainId: currentChainId, type: networkType } = providerConfig;\n const { transactions } = this.state;\n\n const supportedNetworkIds = ['1', '5', '11155111'];\n /* istanbul ignore next */\n if (supportedNetworkIds.indexOf(currentNetworkID) === -1) {\n return undefined;\n }\n\n const [etherscanTxResponse, etherscanTokenResponse] =\n await handleTransactionFetch(\n networkType,\n address,\n this.config.txHistoryLimit,\n opt,\n );\n\n const normalizedTxs = etherscanTxResponse.result.map(\n (tx: EtherscanTransactionMeta) =>\n this.normalizeTx(tx, currentNetworkID, currentChainId),\n );\n const normalizedTokenTxs = etherscanTokenResponse.result.map(\n (tx: EtherscanTransactionMeta) =>\n this.normalizeTokenTx(tx, currentNetworkID, currentChainId),\n );\n\n const [updateRequired, allTxs] = this.etherscanTransactionStateReconciler(\n [...normalizedTxs, ...normalizedTokenTxs],\n transactions,\n );\n\n allTxs.sort((a, b) => (a.time < b.time ? -1 : 1));\n\n let latestIncomingTxBlockNumber: string | undefined;\n allTxs.forEach(async (tx) => {\n /* istanbul ignore next */\n if (\n // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.\n (tx.chainId === currentChainId ||\n (!tx.chainId && tx.networkID === currentNetworkID)) &&\n tx.transaction.to &&\n tx.transaction.to.toLowerCase() === address.toLowerCase()\n ) {\n if (\n tx.blockNumber &&\n (!latestIncomingTxBlockNumber ||\n parseInt(latestIncomingTxBlockNumber, 10) <\n parseInt(tx.blockNumber, 10))\n ) {\n latestIncomingTxBlockNumber = tx.blockNumber;\n }\n }\n\n /* istanbul ignore else */\n if (tx.toSmartContract === undefined) {\n // If not `to` is a contract deploy, if not `data` is send eth\n if (\n tx.transaction.to &&\n (!tx.transaction.data || tx.transaction.data !== '0x')\n ) {\n const code = await query(this.ethQuery, 'getCode', [\n tx.transaction.to,\n ]);\n tx.toSmartContract = isSmartContractCode(code);\n } else {\n tx.toSmartContract = false;\n }\n }\n });\n\n // Update state only if new transactions were fetched or\n // the status or gas data of a transaction has changed\n if (updateRequired) {\n this.update({ transactions: this.trimTransactionsForState(allTxs) });\n }\n return latestIncomingTxBlockNumber;\n }\n\n /**\n * Trim the amount of transactions that are set on the state. Checks\n * if the length of the tx history is longer then desired persistence\n * limit and then if it is removes the oldest confirmed or rejected tx.\n * Pending or unapproved transactions will not be removed by this\n * operation. For safety of presenting a fully functional transaction UI\n * representation, this function will not break apart transactions with the\n * same nonce, created on the same day, per network. Not accounting for transactions of the same\n * nonce, same day and network combo can result in confusing or broken experiences\n * in the UI. The transactions are then updated using the BaseController update.\n *\n * @param transactions - The transactions to be applied to the state.\n * @returns The trimmed list of transactions.\n */\n private trimTransactionsForState(\n transactions: TransactionMeta[],\n ): TransactionMeta[] {\n const nonceNetworkSet = new Set();\n const txsToKeep = transactions.reverse().filter((tx) => {\n const { chainId, networkID, status, transaction, time } = tx;\n if (transaction) {\n const key = `${transaction.nonce}-${chainId ?? networkID}-${new Date(\n time,\n ).toDateString()}`;\n if (nonceNetworkSet.has(key)) {\n return true;\n } else if (\n nonceNetworkSet.size < this.config.txHistoryLimit ||\n !this.isFinalState(status)\n ) {\n nonceNetworkSet.add(key);\n return true;\n }\n }\n return false;\n });\n txsToKeep.reverse();\n return txsToKeep;\n }\n\n /**\n * Determines if the transaction is in a final state.\n *\n * @param status - The transaction status.\n * @returns Whether the transaction is in a final state.\n */\n private isFinalState(status: TransactionStatus): boolean {\n return (\n status === TransactionStatus.rejected ||\n status === TransactionStatus.confirmed ||\n status === TransactionStatus.failed ||\n status === TransactionStatus.cancelled\n );\n }\n\n /**\n * Method to verify the state of a transaction using the Blockchain as a source of truth.\n *\n * @param meta - The local transaction to verify on the blockchain.\n * @returns A tuple containing the updated transaction, and whether or not an update was required.\n */\n private async blockchainTransactionStateReconciler(\n meta: TransactionMeta,\n ): Promise<[TransactionMeta, boolean]> {\n const { status, transactionHash } = meta;\n switch (status) {\n case TransactionStatus.confirmed:\n const txReceipt = await query(this.ethQuery, 'getTransactionReceipt', [\n transactionHash,\n ]);\n\n if (!txReceipt) {\n return [meta, false];\n }\n\n meta.verifiedOnBlockchain = true;\n meta.transaction.gasUsed = txReceipt.gasUsed;\n\n // According to the Web3 docs:\n // TRUE if the transaction was successful, FALSE if the EVM reverted the transaction.\n if (Number(txReceipt.status) === 0) {\n const error: Error = new Error(\n 'Transaction failed. The transaction was reversed',\n );\n this.failTransaction(meta, error);\n return [meta, false];\n }\n\n return [meta, true];\n case TransactionStatus.submitted:\n const txObj = await query(this.ethQuery, 'getTransactionByHash', [\n transactionHash,\n ]);\n\n if (!txObj) {\n const receiptShowsFailedStatus =\n await this.checkTxReceiptStatusIsFailed(transactionHash);\n\n // Case the txObj is evaluated as false, a second check will\n // determine if the tx failed or it is pending or confirmed\n if (receiptShowsFailedStatus) {\n const error: Error = new Error(\n 'Transaction failed. The transaction was dropped or replaced by a new one',\n );\n this.failTransaction(meta, error);\n }\n }\n\n /* istanbul ignore next */\n if (txObj?.blockNumber) {\n meta.status = TransactionStatus.confirmed;\n this.hub.emit(`${meta.id}:confirmed`, meta);\n return [meta, true];\n }\n\n return [meta, false];\n default:\n return [meta, false];\n }\n }\n\n /**\n * Method to check if a tx has failed according to their receipt\n * According to the Web3 docs:\n * TRUE if the transaction was successful, FALSE if the EVM reverted the transaction.\n * The receipt is not available for pending transactions and returns null.\n *\n * @param txHash - The transaction hash.\n * @returns Whether the transaction has failed.\n */\n private async checkTxReceiptStatusIsFailed(\n txHash: string | undefined,\n ): Promise {\n const txReceipt = await query(this.ethQuery, 'getTransactionReceipt', [\n txHash,\n ]);\n if (!txReceipt) {\n // Transaction is pending\n return false;\n }\n return Number(txReceipt.status) === 0;\n }\n\n /**\n * Method to verify the state of transactions using Etherscan as a source of truth.\n *\n * @param remoteTxs - Transactions to reconcile that are from a remote source.\n * @param localTxs - Transactions to reconcile that are local.\n * @returns A tuple containing a boolean indicating whether or not an update was required, and the updated transaction.\n */\n private etherscanTransactionStateReconciler(\n remoteTxs: TransactionMeta[],\n localTxs: TransactionMeta[],\n ): [boolean, TransactionMeta[]] {\n const updatedTxs: TransactionMeta[] = this.getUpdatedTransactions(\n remoteTxs,\n localTxs,\n );\n\n const newTxs: TransactionMeta[] = this.getNewTransactions(\n remoteTxs,\n localTxs,\n );\n\n const updatedLocalTxs = localTxs.map((tx: TransactionMeta) => {\n const txIdx = updatedTxs.findIndex(\n ({ transactionHash }) => transactionHash === tx.transactionHash,\n );\n return txIdx === -1 ? tx : updatedTxs[txIdx];\n });\n\n const updateRequired = newTxs.length > 0 || updatedLocalTxs.length > 0;\n\n return [updateRequired, [...newTxs, ...updatedLocalTxs]];\n }\n\n /**\n * Get all transactions that are in the remote transactions array\n * but not in the local transactions array.\n *\n * @param remoteTxs - Array of transactions from remote source.\n * @param localTxs - Array of transactions stored locally.\n * @returns The new transactions.\n */\n private getNewTransactions(\n remoteTxs: TransactionMeta[],\n localTxs: TransactionMeta[],\n ): TransactionMeta[] {\n return remoteTxs.filter((tx) => {\n const alreadyInTransactions = localTxs.find(\n ({ transactionHash }) => transactionHash === tx.transactionHash,\n );\n return !alreadyInTransactions;\n });\n }\n\n /**\n * Get all the transactions that are locally outdated with respect\n * to a remote source (etherscan or blockchain). The returned array\n * contains the transactions with the updated data.\n *\n * @param remoteTxs - Array of transactions from remote source.\n * @param localTxs - Array of transactions stored locally.\n * @returns The updated transactions.\n */\n private getUpdatedTransactions(\n remoteTxs: TransactionMeta[],\n localTxs: TransactionMeta[],\n ): TransactionMeta[] {\n return remoteTxs.filter((remoteTx) => {\n const isTxOutdated = localTxs.find((localTx) => {\n return (\n remoteTx.transactionHash === localTx.transactionHash &&\n this.isTransactionOutdated(remoteTx, localTx)\n );\n });\n return isTxOutdated;\n });\n }\n\n /**\n * Verifies if a local transaction is outdated with respect to the remote transaction.\n *\n * @param remoteTx - The remote transaction from Etherscan.\n * @param localTx - The local transaction.\n * @returns Whether the transaction is outdated.\n */\n private isTransactionOutdated(\n remoteTx: TransactionMeta,\n localTx: TransactionMeta,\n ): boolean {\n const statusOutdated = this.isStatusOutdated(\n remoteTx.transactionHash,\n localTx.transactionHash,\n remoteTx.status,\n localTx.status,\n );\n const gasDataOutdated = this.isGasDataOutdated(\n remoteTx.transaction.gasUsed,\n localTx.transaction.gasUsed,\n );\n return statusOutdated || gasDataOutdated;\n }\n\n /**\n * Verifies if the status of a local transaction is outdated with respect to the remote transaction.\n *\n * @param remoteTxHash - Remote transaction hash.\n * @param localTxHash - Local transaction hash.\n * @param remoteTxStatus - Remote transaction status.\n * @param localTxStatus - Local transaction status.\n * @returns Whether the status is outdated.\n */\n private isStatusOutdated(\n remoteTxHash: string | undefined,\n localTxHash: string | undefined,\n remoteTxStatus: TransactionStatus,\n localTxStatus: TransactionStatus,\n ): boolean {\n return remoteTxHash === localTxHash && remoteTxStatus !== localTxStatus;\n }\n\n /**\n * Verifies if the gas data of a local transaction is outdated with respect to the remote transaction.\n *\n * @param remoteGasUsed - Remote gas used in the transaction.\n * @param localGasUsed - Local gas used in the transaction.\n * @returns Whether the gas data is outdated.\n */\n private isGasDataOutdated(\n remoteGasUsed: string | undefined,\n localGasUsed: string | undefined,\n ): boolean {\n return remoteGasUsed !== localGasUsed;\n }\n}\n\nexport default TransactionController;\n"]}
+\ No newline at end of file
++{"version":3,"file":"TransactionController.js","sourceRoot":"","sources":["../src/TransactionController.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from 'events';\nimport { addHexPrefix, bufferToHex, BN } from 'ethereumjs-util';\nimport { ethErrors } from 'eth-rpc-errors';\nimport MethodRegistry from 'eth-method-registry';\nimport EthQuery from 'eth-query';\nimport Common from '@ethereumjs/common';\nimport { TransactionFactory, TypedTransaction } from '@ethereumjs/tx';\nimport { v1 as random } from 'uuid';\nimport { Mutex } from 'async-mutex';\nimport {\n BaseController,\n BaseConfig,\n BaseState,\n RestrictedControllerMessenger,\n} from '@metamask/base-controller';\nimport type {\n NetworkState,\n NetworkController,\n} from '@metamask/network-controller';\nimport {\n BNToHex,\n fractionBN,\n hexToBN,\n safelyExecute,\n isSmartContractCode,\n query,\n MAINNET,\n RPC,\n} from '@metamask/controller-utils';\nimport {\n AcceptRequest as AcceptApprovalRequest,\n AddApprovalRequest,\n RejectRequest as RejectApprovalRequest,\n} from '@metamask/approval-controller';\nimport {\n normalizeTransaction,\n validateTransaction,\n handleTransactionFetch,\n getIncreasedPriceFromExisting,\n isEIP1559Transaction,\n isGasPriceValue,\n isFeeMarketEIP1559Values,\n validateGasValues,\n validateMinimumIncrease,\n ESTIMATE_GAS_ERROR,\n} from './utils';\n\nconst HARDFORK = 'london';\n\n/**\n * @type Result\n * @property result - Promise resolving to a new transaction hash\n * @property transactionMeta - Meta information about this new transaction\n */\nexport interface Result {\n result: Promise;\n transactionMeta: TransactionMeta;\n}\n\n/**\n * @type Fetch All Options\n * @property fromBlock - String containing a specific block decimal number\n * @property etherscanApiKey - API key to be used to fetch token transactions\n */\nexport interface FetchAllOptions {\n fromBlock?: string;\n etherscanApiKey?: string;\n}\n\n/**\n * @type Transaction\n *\n * Transaction representation\n * @property chainId - Network ID as per EIP-155\n * @property data - Data to pass with this transaction\n * @property from - Address to send this transaction from\n * @property gas - Gas to send with this transaction\n * @property gasPrice - Price of gas with this transaction\n * @property gasUsed - Gas used in the transaction\n * @property nonce - Unique number to prevent replay attacks\n * @property to - Address to send this transaction to\n * @property value - Value associated with this transaction\n */\nexport interface Transaction {\n chainId?: number;\n data?: string;\n from: string;\n gas?: string;\n gasPrice?: string;\n gasUsed?: string;\n nonce?: string;\n to?: string;\n value?: string;\n maxFeePerGas?: string;\n maxPriorityFeePerGas?: string;\n estimatedBaseFee?: string;\n estimateGasError?: string;\n}\n\nexport interface GasPriceValue {\n gasPrice: string;\n}\n\nexport interface FeeMarketEIP1559Values {\n maxFeePerGas: string;\n maxPriorityFeePerGas: string;\n}\n\n/**\n * The status of the transaction. Each status represents the state of the transaction internally\n * in the wallet. Some of these correspond with the state of the transaction on the network, but\n * some are wallet-specific.\n */\nexport enum TransactionStatus {\n approved = 'approved',\n cancelled = 'cancelled',\n confirmed = 'confirmed',\n failed = 'failed',\n rejected = 'rejected',\n signed = 'signed',\n submitted = 'submitted',\n unapproved = 'unapproved',\n}\n\n/**\n * Options for wallet device.\n */\nexport enum WalletDevice {\n MM_MOBILE = 'metamask_mobile',\n MM_EXTENSION = 'metamask_extension',\n OTHER = 'other_device',\n}\n\ntype TransactionMetaBase = {\n isTransfer?: boolean;\n transferInformation?: {\n symbol: string;\n contractAddress: string;\n decimals: number;\n };\n id: string;\n networkID?: string;\n chainId?: string;\n origin?: string;\n rawTransaction?: string;\n time: number;\n toSmartContract?: boolean;\n transaction: Transaction;\n transactionHash?: string;\n blockNumber?: string;\n deviceConfirmedOn?: WalletDevice;\n verifiedOnBlockchain?: boolean;\n};\n\n/**\n * @type TransactionMeta\n *\n * TransactionMeta representation\n * @property error - Synthesized error information for failed transactions\n * @property id - Generated UUID associated with this transaction\n * @property networkID - Network code as per EIP-155 for this transaction\n * @property origin - Origin this transaction was sent from\n * @property deviceConfirmedOn - string to indicate what device the transaction was confirmed\n * @property rawTransaction - Hex representation of the underlying transaction\n * @property status - String status of this transaction\n * @property time - Timestamp associated with this transaction\n * @property toSmartContract - Whether transaction recipient is a smart contract\n * @property transaction - Underlying Transaction object\n * @property transactionHash - Hash of a successful transaction\n * @property blockNumber - Number of the block where the transaction has been included\n */\nexport type TransactionMeta =\n | ({\n status: Exclude;\n } & TransactionMetaBase)\n | ({ status: TransactionStatus.failed; error: Error } & TransactionMetaBase);\n\n/**\n * @type EtherscanTransactionMeta\n *\n * EtherscanTransactionMeta representation\n * @property blockNumber - Number of the block where the transaction has been included\n * @property timeStamp - Timestamp associated with this transaction\n * @property hash - Hash of a successful transaction\n * @property nonce - Nonce of the transaction\n * @property blockHash - Hash of the block where the transaction has been included\n * @property transactionIndex - Etherscan internal index for this transaction\n * @property from - Address to send this transaction from\n * @property to - Address to send this transaction to\n * @property gas - Gas to send with this transaction\n * @property gasPrice - Price of gas with this transaction\n * @property isError - Synthesized error information for failed transactions\n * @property txreceipt_status - Receipt status for this transaction\n * @property input - input of the transaction\n * @property contractAddress - Address of the contract\n * @property cumulativeGasUsed - Amount of gas used\n * @property confirmations - Number of confirmations\n */\nexport interface EtherscanTransactionMeta {\n blockNumber: string;\n timeStamp: string;\n hash: string;\n nonce: string;\n blockHash: string;\n transactionIndex: string;\n from: string;\n to: string;\n value: string;\n gas: string;\n gasPrice: string;\n cumulativeGasUsed: string;\n gasUsed: string;\n isError: string;\n txreceipt_status: string;\n input: string;\n contractAddress: string;\n confirmations: string;\n tokenDecimal: string;\n tokenSymbol: string;\n}\n\n/**\n * @type TransactionConfig\n *\n * Transaction controller configuration\n * @property interval - Polling interval used to fetch new currency rate\n * @property provider - Provider used to create a new underlying EthQuery instance\n * @property sign - Method used to sign transactions\n */\nexport interface TransactionConfig extends BaseConfig {\n interval: number;\n sign?: (transaction: Transaction, from: string) => Promise;\n txHistoryLimit: number;\n}\n\n/**\n * @type MethodData\n *\n * Method data registry object\n * @property registryMethod - Registry method raw string\n * @property parsedRegistryMethod - Registry method object, containing name and method arguments\n */\nexport interface MethodData {\n registryMethod: string;\n parsedRegistryMethod: Record;\n}\n\n/**\n * @type TransactionState\n *\n * Transaction controller state\n * @property transactions - A list of TransactionMeta objects\n * @property methodData - Object containing all known method data information\n */\nexport interface TransactionState extends BaseState {\n transactions: TransactionMeta[];\n methodData: { [key: string]: MethodData };\n}\n\n/**\n * Multiplier used to determine a transaction's increased gas fee during cancellation\n */\nexport const CANCEL_RATE = 1.5;\n\n/**\n * Multiplier used to determine a transaction's increased gas fee during speed up\n */\nexport const SPEED_UP_RATE = 1.1;\n\n/**\n * The name of the {@link TransactionController}.\n */\nconst controllerName = 'TransactionController';\n\n/**\n * The external actions available to the {@link TransactionController}.\n */\ntype AllowedActions =\n | AddApprovalRequest\n | AcceptApprovalRequest\n | RejectApprovalRequest;\n\n/**\n * The messenger of the {@link TransactionController}.\n */\nexport type TransactionControllerMessenger = RestrictedControllerMessenger<\n typeof controllerName,\n AllowedActions,\n never,\n AllowedActions['type'],\n never\n>;\n\n/**\n * Controller responsible for submitting and managing transactions.\n */\nexport class TransactionController extends BaseController<\n TransactionConfig,\n TransactionState\n> {\n private ethQuery: any;\n\n private registry: any;\n\n private handle?: ReturnType;\n\n private mutex = new Mutex();\n\n private getNetworkState: () => NetworkState;\n\n private messagingSystem: TransactionControllerMessenger;\n\n private failTransaction(transactionMeta: TransactionMeta, error: Error) {\n const newTransactionMeta = {\n ...transactionMeta,\n error,\n status: TransactionStatus.failed,\n };\n this.updateTransaction(newTransactionMeta);\n this.hub.emit(`${transactionMeta.id}:finished`, newTransactionMeta);\n }\n\n private async registryLookup(fourBytePrefix: string): Promise {\n const registryMethod = await this.registry.lookup(fourBytePrefix);\n const parsedRegistryMethod = this.registry.parse(registryMethod);\n return { registryMethod, parsedRegistryMethod };\n }\n\n /**\n * Normalizes the transaction information from etherscan\n * to be compatible with the TransactionMeta interface.\n *\n * @param txMeta - The transaction.\n * @param currentNetworkID - The current network ID.\n * @param currentChainId - The current chain ID.\n * @returns The normalized transaction.\n */\n private normalizeTx(\n txMeta: EtherscanTransactionMeta,\n currentNetworkID: string,\n currentChainId: string,\n ): TransactionMeta {\n const time = parseInt(txMeta.timeStamp, 10) * 1000;\n const normalizedTransactionBase = {\n blockNumber: txMeta.blockNumber,\n id: random({ msecs: time }),\n networkID: currentNetworkID,\n chainId: currentChainId,\n time,\n transaction: {\n data: txMeta.input,\n from: txMeta.from,\n gas: BNToHex(new BN(txMeta.gas)),\n gasPrice: BNToHex(new BN(txMeta.gasPrice)),\n gasUsed: BNToHex(new BN(txMeta.gasUsed)),\n nonce: BNToHex(new BN(txMeta.nonce)),\n to: txMeta.to,\n value: BNToHex(new BN(txMeta.value)),\n },\n transactionHash: txMeta.hash,\n verifiedOnBlockchain: false,\n };\n\n /* istanbul ignore else */\n if (txMeta.isError === '0') {\n return {\n ...normalizedTransactionBase,\n status: TransactionStatus.confirmed,\n };\n }\n\n /* istanbul ignore next */\n return {\n ...normalizedTransactionBase,\n error: new Error('Transaction failed'),\n status: TransactionStatus.failed,\n };\n }\n\n private normalizeTokenTx = (\n txMeta: EtherscanTransactionMeta,\n currentNetworkID: string,\n currentChainId: string,\n ): TransactionMeta => {\n const time = parseInt(txMeta.timeStamp, 10) * 1000;\n const {\n to,\n from,\n gas,\n gasPrice,\n gasUsed,\n hash,\n contractAddress,\n tokenDecimal,\n tokenSymbol,\n value,\n } = txMeta;\n return {\n id: random({ msecs: time }),\n isTransfer: true,\n networkID: currentNetworkID,\n chainId: currentChainId,\n status: TransactionStatus.confirmed,\n time,\n transaction: {\n chainId: 1,\n from,\n gas,\n gasPrice,\n gasUsed,\n to,\n value,\n },\n transactionHash: hash,\n transferInformation: {\n contractAddress,\n decimals: Number(tokenDecimal),\n symbol: tokenSymbol,\n },\n verifiedOnBlockchain: false,\n };\n };\n\n /**\n * EventEmitter instance used to listen to specific transactional events\n */\n hub = new EventEmitter();\n\n /**\n * Name of this controller used during composition\n */\n override name = 'TransactionController';\n\n /**\n * Method used to sign transactions\n */\n sign?: (\n transaction: TypedTransaction,\n from: string,\n ) => Promise;\n\n /**\n * Creates a TransactionController instance.\n *\n * @param options - The controller options.\n * @param options.getNetworkState - Gets the state of the network controller.\n * @param options.onNetworkStateChange - Allows subscribing to network controller state changes.\n * @param options.getProvider - Returns a provider for the current network.\n * @param options.messenger - The controller messenger.\n * @param config - Initial options used to configure this controller.\n * @param state - Initial state to set on this controller.\n */\n constructor(\n {\n getNetworkState,\n onNetworkStateChange,\n getProvider,\n messenger,\n }: {\n getNetworkState: () => NetworkState;\n onNetworkStateChange: (listener: (state: NetworkState) => void) => void;\n getProvider: () => NetworkController['provider'];\n messenger: TransactionControllerMessenger;\n },\n config?: Partial,\n state?: Partial,\n ) {\n super(config, state);\n this.defaultConfig = {\n interval: 15000,\n txHistoryLimit: 40,\n };\n\n this.defaultState = {\n methodData: {},\n transactions: [],\n };\n this.initialize();\n const provider = getProvider();\n this.messagingSystem = messenger;\n this.getNetworkState = getNetworkState;\n this.ethQuery = new EthQuery(provider);\n this.registry = new MethodRegistry({ provider });\n\n onNetworkStateChange(() => {\n const newProvider = getProvider();\n this.ethQuery = new EthQuery(newProvider);\n this.registry = new MethodRegistry({ provider: newProvider });\n });\n this.poll();\n }\n\n /**\n * Starts a new polling interval.\n *\n * @param interval - The polling interval used to fetch new transaction statuses.\n */\n async poll(interval?: number): Promise {\n interval && this.configure({ interval }, false, false);\n this.handle && clearTimeout(this.handle);\n await safelyExecute(() => this.queryTransactionStatuses());\n this.handle = setTimeout(() => {\n this.poll(this.config.interval);\n }, this.config.interval);\n }\n\n /**\n * Handle new method data request.\n *\n * @param fourBytePrefix - The method prefix.\n * @returns The method data object corresponding to the given signature prefix.\n */\n async handleMethodData(fourBytePrefix: string): Promise {\n const releaseLock = await this.mutex.acquire();\n try {\n const { methodData } = this.state;\n const knownMethod = Object.keys(methodData).find(\n (knownFourBytePrefix) => fourBytePrefix === knownFourBytePrefix,\n );\n if (knownMethod) {\n return methodData[fourBytePrefix];\n }\n const registry = await this.registryLookup(fourBytePrefix);\n this.update({\n methodData: { ...methodData, ...{ [fourBytePrefix]: registry } },\n });\n return registry;\n } finally {\n releaseLock();\n }\n }\n\n /**\n * Add a new unapproved transaction to state. Parameters will be validated, a\n * unique transaction id will be generated, and gas and gasPrice will be calculated\n * if not provided. If A `:unapproved` hub event will be emitted once added.\n *\n * @param transaction - The transaction object to add.\n * @param origin - The domain origin to append to the generated TransactionMeta.\n * @param deviceConfirmedOn - An enum to indicate what device the transaction was confirmed to append to the generated TransactionMeta.\n * @returns Object containing a promise resolving to the transaction hash if approved.\n */\n async addTransaction(\n transaction: Transaction,\n origin?: string,\n deviceConfirmedOn?: WalletDevice,\n ): Promise {\n const { providerConfig, network } = this.getNetworkState();\n const { transactions } = this.state;\n transaction = normalizeTransaction(transaction);\n validateTransaction(transaction);\n\n const transactionMeta: TransactionMeta = {\n id: random(),\n networkID: network ?? undefined,\n chainId: providerConfig.chainId,\n origin,\n status: TransactionStatus.unapproved as TransactionStatus.unapproved,\n time: Date.now(),\n transaction,\n deviceConfirmedOn,\n verifiedOnBlockchain: false,\n };\n\n try {\n const { gas, estimateGasError } = await this.estimateGas(transaction);\n transaction.gas = gas;\n transaction.estimateGasError = estimateGasError;\n } catch (error: any) {\n this.failTransaction(transactionMeta, error);\n return Promise.reject(error);\n }\n\n const result: Promise = new Promise((resolve, reject) => {\n this.hub.once(\n `${transactionMeta.id}:finished`,\n (meta: TransactionMeta) => {\n switch (meta.status) {\n case TransactionStatus.submitted:\n return resolve(meta.transactionHash as string);\n case TransactionStatus.rejected:\n return reject(\n ethErrors.provider.userRejectedRequest(\n 'User rejected the transaction',\n ),\n );\n case TransactionStatus.cancelled:\n return reject(\n ethErrors.rpc.internal('User cancelled the transaction'),\n );\n case TransactionStatus.failed:\n return reject(ethErrors.rpc.internal(meta.error.message));\n /* istanbul ignore next */\n default:\n return reject(\n ethErrors.rpc.internal(\n `MetaMask Tx Signature: Unknown problem: ${JSON.stringify(\n meta,\n )}`,\n ),\n );\n }\n },\n );\n });\n\n transactions.push(transactionMeta);\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n this.hub.emit(`unapprovedTransaction`, transactionMeta);\n this.requestApproval(transactionMeta);\n return { result, transactionMeta };\n }\n\n prepareUnsignedEthTx(txParams: Record): TypedTransaction {\n return TransactionFactory.fromTxData(txParams, {\n common: this.getCommonConfiguration(),\n freeze: false,\n });\n }\n\n /**\n * `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for\n * specifying which chain, network, hardfork and EIPs to support for\n * a transaction. By referencing this configuration, and analyzing the fields\n * specified in txParams, @ethereumjs/tx is able to determine which EIP-2718\n * transaction type to use.\n *\n * @returns {Common} common configuration object\n */\n\n getCommonConfiguration(): Common {\n const {\n network: networkId,\n providerConfig: { type: chain, chainId, nickname: name },\n } = this.getNetworkState();\n\n if (chain !== RPC) {\n return new Common({ chain, hardfork: HARDFORK });\n }\n\n const customChainParams = {\n name,\n chainId: parseInt(chainId, undefined),\n networkId: parseInt(networkId, undefined),\n };\n\n return Common.forCustomChain(MAINNET, customChainParams, HARDFORK);\n }\n\n /**\n * Approves a transaction and updates it's status in state. If this is not a\n * retry transaction, a nonce will be generated. The transaction is signed\n * using the sign configuration property, then published to the blockchain.\n * A `:finished` hub event is fired after success or failure.\n *\n * @param transactionID - The ID of the transaction to approve.\n */\n async approveTransaction(transactionID: string) {\n const { transactions } = this.state;\n const releaseLock = await this.mutex.acquire();\n const { providerConfig } = this.getNetworkState();\n const { chainId: currentChainId } = providerConfig;\n const index = transactions.findIndex(({ id }) => transactionID === id);\n const transactionMeta = transactions[index];\n const { nonce } = transactionMeta.transaction;\n try {\n const { from } = transactionMeta.transaction;\n if (!this.sign) {\n releaseLock();\n this.failTransaction(\n transactionMeta,\n new Error('No sign method defined.'),\n );\n this.rejectApproval(transactionMeta);\n return;\n } else if (!currentChainId) {\n releaseLock();\n this.failTransaction(transactionMeta, new Error('No chainId defined.'));\n this.rejectApproval(transactionMeta);\n return;\n }\n\n const chainId = parseInt(currentChainId, undefined);\n const { approved: status } = TransactionStatus;\n const txNonce =\n nonce ||\n (await query(this.ethQuery, 'getTransactionCount', [from, 'pending']));\n\n transactionMeta.status = status;\n transactionMeta.transaction.nonce = txNonce;\n transactionMeta.transaction.chainId = chainId;\n\n const baseTxParams = {\n ...transactionMeta.transaction,\n gasLimit: transactionMeta.transaction.gas,\n chainId,\n nonce: txNonce,\n status,\n };\n\n const isEIP1559 = isEIP1559Transaction(transactionMeta.transaction);\n\n const txParams = isEIP1559\n ? {\n ...baseTxParams,\n maxFeePerGas: transactionMeta.transaction.maxFeePerGas,\n maxPriorityFeePerGas:\n transactionMeta.transaction.maxPriorityFeePerGas,\n estimatedBaseFee: transactionMeta.transaction.estimatedBaseFee,\n // specify type 2 if maxFeePerGas and maxPriorityFeePerGas are set\n type: 2,\n }\n : baseTxParams;\n\n // delete gasPrice if maxFeePerGas and maxPriorityFeePerGas are set\n if (isEIP1559) {\n delete txParams.gasPrice;\n }\n\n const unsignedEthTx = this.prepareUnsignedEthTx(txParams);\n const signedTx = await this.sign(unsignedEthTx, from);\n transactionMeta.status = TransactionStatus.signed;\n this.updateTransaction(transactionMeta);\n const rawTransaction = bufferToHex(signedTx.serialize());\n\n transactionMeta.rawTransaction = rawTransaction;\n this.updateTransaction(transactionMeta);\n const transactionHash = await query(this.ethQuery, 'sendRawTransaction', [\n rawTransaction,\n ]);\n transactionMeta.transactionHash = transactionHash;\n transactionMeta.status = TransactionStatus.submitted;\n this.updateTransaction(transactionMeta);\n this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);\n this.acceptApproval(transactionMeta);\n } catch (error: any) {\n this.failTransaction(transactionMeta, error);\n this.rejectApproval(transactionMeta);\n } finally {\n releaseLock();\n }\n }\n\n /**\n * Cancels a transaction based on its ID by setting its status to \"rejected\"\n * and emitting a `:finished` hub event.\n *\n * @param transactionID - The ID of the transaction to cancel.\n */\n cancelTransaction(transactionID: string) {\n const transactionMeta = this.state.transactions.find(\n ({ id }) => id === transactionID,\n );\n if (!transactionMeta) {\n return;\n }\n transactionMeta.status = TransactionStatus.rejected;\n this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);\n const transactions = this.state.transactions.filter(\n ({ id }) => id !== transactionID,\n );\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n this.rejectApproval(transactionMeta);\n }\n\n /**\n * Attempts to cancel a transaction based on its ID by setting its status to \"rejected\"\n * and emitting a `:finished` hub event.\n *\n * @param transactionID - The ID of the transaction to cancel.\n * @param gasValues - The gas values to use for the cancellation transaction.\n */\n async stopTransaction(\n transactionID: string,\n gasValues?: GasPriceValue | FeeMarketEIP1559Values,\n ) {\n if (gasValues) {\n validateGasValues(gasValues);\n }\n const transactionMeta = this.state.transactions.find(\n ({ id }) => id === transactionID,\n );\n if (!transactionMeta) {\n return;\n }\n\n if (!this.sign) {\n throw new Error('No sign method defined.');\n }\n\n // gasPrice (legacy non EIP1559)\n const minGasPrice = getIncreasedPriceFromExisting(\n transactionMeta.transaction.gasPrice,\n CANCEL_RATE,\n );\n\n const gasPriceFromValues = isGasPriceValue(gasValues) && gasValues.gasPrice;\n\n const newGasPrice =\n (gasPriceFromValues &&\n validateMinimumIncrease(gasPriceFromValues, minGasPrice)) ||\n minGasPrice;\n\n // maxFeePerGas (EIP1559)\n const existingMaxFeePerGas = transactionMeta.transaction?.maxFeePerGas;\n const minMaxFeePerGas = getIncreasedPriceFromExisting(\n existingMaxFeePerGas,\n CANCEL_RATE,\n );\n const maxFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxFeePerGas;\n const newMaxFeePerGas =\n (maxFeePerGasValues &&\n validateMinimumIncrease(maxFeePerGasValues, minMaxFeePerGas)) ||\n (existingMaxFeePerGas && minMaxFeePerGas);\n\n // maxPriorityFeePerGas (EIP1559)\n const existingMaxPriorityFeePerGas =\n transactionMeta.transaction?.maxPriorityFeePerGas;\n const minMaxPriorityFeePerGas = getIncreasedPriceFromExisting(\n existingMaxPriorityFeePerGas,\n CANCEL_RATE,\n );\n const maxPriorityFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxPriorityFeePerGas;\n const newMaxPriorityFeePerGas =\n (maxPriorityFeePerGasValues &&\n validateMinimumIncrease(\n maxPriorityFeePerGasValues,\n minMaxPriorityFeePerGas,\n )) ||\n (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);\n\n const txParams =\n newMaxFeePerGas && newMaxPriorityFeePerGas\n ? {\n from: transactionMeta.transaction.from,\n gasLimit: transactionMeta.transaction.gas,\n maxFeePerGas: newMaxFeePerGas,\n maxPriorityFeePerGas: newMaxPriorityFeePerGas,\n type: 2,\n nonce: transactionMeta.transaction.nonce,\n to: transactionMeta.transaction.from,\n value: '0x0',\n }\n : {\n from: transactionMeta.transaction.from,\n gasLimit: transactionMeta.transaction.gas,\n gasPrice: newGasPrice,\n nonce: transactionMeta.transaction.nonce,\n to: transactionMeta.transaction.from,\n value: '0x0',\n };\n\n const unsignedEthTx = this.prepareUnsignedEthTx(txParams);\n\n const signedTx = await this.sign(\n unsignedEthTx,\n transactionMeta.transaction.from,\n );\n const rawTransaction = bufferToHex(signedTx.serialize());\n await query(this.ethQuery, 'sendRawTransaction', [rawTransaction]);\n transactionMeta.status = TransactionStatus.cancelled;\n this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);\n this.rejectApproval(transactionMeta);\n }\n\n /**\n * Attempts to speed up a transaction increasing transaction gasPrice by ten percent.\n *\n * @param transactionID - The ID of the transaction to speed up.\n * @param gasValues - The gas values to use for the speed up transation.\n */\n async speedUpTransaction(\n transactionID: string,\n gasValues?: GasPriceValue | FeeMarketEIP1559Values,\n ) {\n if (gasValues) {\n validateGasValues(gasValues);\n }\n const transactionMeta = this.state.transactions.find(\n ({ id }) => id === transactionID,\n );\n /* istanbul ignore next */\n if (!transactionMeta) {\n return;\n }\n\n /* istanbul ignore next */\n if (!this.sign) {\n throw new Error('No sign method defined.');\n }\n\n const { transactions } = this.state;\n\n // gasPrice (legacy non EIP1559)\n const minGasPrice = getIncreasedPriceFromExisting(\n transactionMeta.transaction.gasPrice,\n SPEED_UP_RATE,\n );\n\n const gasPriceFromValues = isGasPriceValue(gasValues) && gasValues.gasPrice;\n\n const newGasPrice =\n (gasPriceFromValues &&\n validateMinimumIncrease(gasPriceFromValues, minGasPrice)) ||\n minGasPrice;\n\n // maxFeePerGas (EIP1559)\n const existingMaxFeePerGas = transactionMeta.transaction?.maxFeePerGas;\n const minMaxFeePerGas = getIncreasedPriceFromExisting(\n existingMaxFeePerGas,\n SPEED_UP_RATE,\n );\n const maxFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxFeePerGas;\n const newMaxFeePerGas =\n (maxFeePerGasValues &&\n validateMinimumIncrease(maxFeePerGasValues, minMaxFeePerGas)) ||\n (existingMaxFeePerGas && minMaxFeePerGas);\n\n // maxPriorityFeePerGas (EIP1559)\n const existingMaxPriorityFeePerGas =\n transactionMeta.transaction?.maxPriorityFeePerGas;\n const minMaxPriorityFeePerGas = getIncreasedPriceFromExisting(\n existingMaxPriorityFeePerGas,\n SPEED_UP_RATE,\n );\n const maxPriorityFeePerGasValues =\n isFeeMarketEIP1559Values(gasValues) && gasValues.maxPriorityFeePerGas;\n const newMaxPriorityFeePerGas =\n (maxPriorityFeePerGasValues &&\n validateMinimumIncrease(\n maxPriorityFeePerGasValues,\n minMaxPriorityFeePerGas,\n )) ||\n (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);\n\n const txParams =\n newMaxFeePerGas && newMaxPriorityFeePerGas\n ? {\n ...transactionMeta.transaction,\n gasLimit: transactionMeta.transaction.gas,\n maxFeePerGas: newMaxFeePerGas,\n maxPriorityFeePerGas: newMaxPriorityFeePerGas,\n type: 2,\n }\n : {\n ...transactionMeta.transaction,\n gasLimit: transactionMeta.transaction.gas,\n gasPrice: newGasPrice,\n };\n\n const unsignedEthTx = this.prepareUnsignedEthTx(txParams);\n\n const signedTx = await this.sign(\n unsignedEthTx,\n transactionMeta.transaction.from,\n );\n const rawTransaction = bufferToHex(signedTx.serialize());\n const transactionHash = await query(this.ethQuery, 'sendRawTransaction', [\n rawTransaction,\n ]);\n const baseTransactionMeta = {\n ...transactionMeta,\n id: random(),\n time: Date.now(),\n transactionHash,\n };\n const newTransactionMeta =\n newMaxFeePerGas && newMaxPriorityFeePerGas\n ? {\n ...baseTransactionMeta,\n transaction: {\n ...transactionMeta.transaction,\n maxFeePerGas: newMaxFeePerGas,\n maxPriorityFeePerGas: newMaxPriorityFeePerGas,\n },\n }\n : {\n ...baseTransactionMeta,\n transaction: {\n ...transactionMeta.transaction,\n gasPrice: newGasPrice,\n },\n };\n transactions.push(newTransactionMeta);\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta);\n }\n\n /**\n * Estimates required gas for a given transaction.\n *\n * @param transaction - The transaction to estimate gas for.\n * @returns The gas and gas price.\n */\n async estimateGas(transaction: Transaction) {\n const estimatedTransaction = { ...transaction };\n const {\n gas,\n gasPrice: providedGasPrice,\n to,\n value,\n data,\n } = estimatedTransaction;\n const gasPrice =\n typeof providedGasPrice === 'undefined'\n ? await query(this.ethQuery, 'gasPrice')\n : providedGasPrice;\n const { isCustomNetwork } = this.getNetworkState();\n // 1. If gas is already defined on the transaction, use it\n if (typeof gas !== 'undefined') {\n return { gas, gasPrice };\n }\n const { gasLimit } = await query(this.ethQuery, 'getBlockByNumber', [\n 'latest',\n false,\n ]);\n\n // 2. If to is not defined or this is not a contract address, and there is no data use 0x5208 / 21000.\n // If the newtwork is a custom network then bypass this check and fetch 'estimateGas'.\n /* istanbul ignore next */\n const code = to ? await query(this.ethQuery, 'getCode', [to]) : undefined;\n /* istanbul ignore next */\n if (\n !isCustomNetwork &&\n (!to || (to && !data && (!code || code === '0x')))\n ) {\n return { gas: '0x5208', gasPrice };\n }\n\n // if data, should be hex string format\n estimatedTransaction.data = !data\n ? data\n : /* istanbul ignore next */ addHexPrefix(data);\n\n // 3. If this is a contract address, safely estimate gas using RPC\n estimatedTransaction.value =\n typeof value === 'undefined' ? '0x0' : /* istanbul ignore next */ value;\n const gasLimitBN = hexToBN(gasLimit);\n estimatedTransaction.gas = BNToHex(fractionBN(gasLimitBN, 19, 20));\n\n let gasHex;\n let estimateGasError;\n try {\n gasHex = await query(this.ethQuery, 'estimateGas', [\n estimatedTransaction,\n ]);\n } catch (error) {\n estimateGasError = ESTIMATE_GAS_ERROR;\n }\n // 4. Pad estimated gas without exceeding the most recent block gasLimit. If the network is a\n // a custom network then return the eth_estimateGas value.\n const gasBN = hexToBN(gasHex);\n const maxGasBN = gasLimitBN.muln(0.9);\n const paddedGasBN = gasBN.muln(1.5);\n /* istanbul ignore next */\n if (gasBN.gt(maxGasBN) || isCustomNetwork) {\n return { gas: addHexPrefix(gasHex), gasPrice, estimateGasError };\n }\n\n /* istanbul ignore next */\n if (paddedGasBN.lt(maxGasBN)) {\n return {\n gas: addHexPrefix(BNToHex(paddedGasBN)),\n gasPrice,\n estimateGasError,\n };\n }\n return { gas: addHexPrefix(BNToHex(maxGasBN)), gasPrice };\n }\n\n /**\n * Check the status of submitted transactions on the network to determine whether they have\n * been included in a block. Any that have been included in a block are marked as confirmed.\n */\n async queryTransactionStatuses() {\n const { transactions } = this.state;\n const { providerConfig, network: currentNetworkID } =\n this.getNetworkState();\n const { chainId: currentChainId } = providerConfig;\n let gotUpdates = false;\n await safelyExecute(() =>\n Promise.all(\n transactions.map(async (meta, index) => {\n // Using fallback to networkID only when there is no chainId present.\n // Should be removed when networkID is completely removed.\n const txBelongsToCurrentChain =\n meta.chainId === currentChainId ||\n (!meta.chainId && meta.networkID === currentNetworkID);\n\n if (!meta.verifiedOnBlockchain && txBelongsToCurrentChain) {\n const [reconciledTx, updateRequired] =\n await this.blockchainTransactionStateReconciler(meta);\n if (updateRequired) {\n transactions[index] = reconciledTx;\n gotUpdates = updateRequired;\n }\n }\n }),\n ),\n );\n\n /* istanbul ignore else */\n if (gotUpdates) {\n this.update({\n transactions: this.trimTransactionsForState(transactions),\n });\n }\n }\n\n /**\n * Updates an existing transaction in state.\n *\n * @param transactionMeta - The new transaction to store in state.\n */\n updateTransaction(transactionMeta: TransactionMeta) {\n const { transactions } = this.state;\n transactionMeta.transaction = normalizeTransaction(\n transactionMeta.transaction,\n );\n validateTransaction(transactionMeta.transaction);\n const index = transactions.findIndex(({ id }) => transactionMeta.id === id);\n transactions[index] = transactionMeta;\n this.update({ transactions: this.trimTransactionsForState(transactions) });\n }\n\n /**\n * Removes all transactions from state, optionally based on the current network.\n *\n * @param ignoreNetwork - Determines whether to wipe all transactions, or just those on the\n * current network. If `true`, all transactions are wiped.\n */\n wipeTransactions(ignoreNetwork?: boolean) {\n /* istanbul ignore next */\n if (ignoreNetwork) {\n this.update({ transactions: [] });\n return;\n }\n const { providerConfig, network: currentNetworkID } =\n this.getNetworkState();\n const { chainId: currentChainId } = providerConfig;\n const newTransactions = this.state.transactions.filter(\n ({ networkID, chainId }) => {\n // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.\n const isCurrentNetwork =\n chainId === currentChainId ||\n (!chainId && networkID === currentNetworkID);\n return !isCurrentNetwork;\n },\n );\n\n this.update({\n transactions: this.trimTransactionsForState(newTransactions),\n });\n }\n\n /**\n * Get transactions from Etherscan for the given address. By default all transactions are\n * returned, but the `fromBlock` option can be given to filter just for transactions from a\n * specific block onward.\n *\n * @param address - The address to fetch the transactions for.\n * @param opt - Object containing optional data, fromBlock and Etherscan API key.\n * @returns The block number of the latest incoming transaction.\n */\n async fetchAll(\n address: string,\n opt?: FetchAllOptions,\n ): Promise {\n const { providerConfig, network: currentNetworkID } =\n this.getNetworkState();\n const { chainId: currentChainId, type: networkType } = providerConfig;\n const { transactions } = this.state;\n\n const supportedNetworkIds = ['1', '5', '11155111'];\n /* istanbul ignore next */\n if (supportedNetworkIds.indexOf(currentNetworkID) === -1) {\n return undefined;\n }\n\n const [etherscanTxResponse, etherscanTokenResponse] =\n await handleTransactionFetch(\n networkType,\n address,\n this.config.txHistoryLimit,\n opt,\n );\n\n const normalizedTxs = etherscanTxResponse.result.map(\n (tx: EtherscanTransactionMeta) =>\n this.normalizeTx(tx, currentNetworkID, currentChainId),\n );\n const normalizedTokenTxs = etherscanTokenResponse.result.map(\n (tx: EtherscanTransactionMeta) =>\n this.normalizeTokenTx(tx, currentNetworkID, currentChainId),\n );\n\n const [updateRequired, allTxs] = this.etherscanTransactionStateReconciler(\n [...normalizedTxs, ...normalizedTokenTxs],\n transactions,\n );\n\n allTxs.sort((a, b) => (a.time < b.time ? -1 : 1));\n\n let latestIncomingTxBlockNumber: string | undefined;\n allTxs.forEach(async (tx) => {\n /* istanbul ignore next */\n if (\n // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.\n (tx.chainId === currentChainId ||\n (!tx.chainId && tx.networkID === currentNetworkID)) &&\n tx.transaction.to &&\n tx.transaction.to.toLowerCase() === address.toLowerCase()\n ) {\n if (\n tx.blockNumber &&\n (!latestIncomingTxBlockNumber ||\n parseInt(latestIncomingTxBlockNumber, 10) <\n parseInt(tx.blockNumber, 10))\n ) {\n latestIncomingTxBlockNumber = tx.blockNumber;\n }\n }\n\n /* istanbul ignore else */\n if (tx.toSmartContract === undefined) {\n // If not `to` is a contract deploy, if not `data` is send eth\n if (\n tx.transaction.to &&\n (!tx.transaction.data || tx.transaction.data !== '0x')\n ) {\n const code = await query(this.ethQuery, 'getCode', [\n tx.transaction.to,\n ]);\n tx.toSmartContract = isSmartContractCode(code);\n } else {\n tx.toSmartContract = false;\n }\n }\n });\n\n // Update state only if new transactions were fetched or\n // the status or gas data of a transaction has changed\n if (updateRequired) {\n this.update({ transactions: this.trimTransactionsForState(allTxs) });\n }\n return latestIncomingTxBlockNumber;\n }\n\n /**\n * Trim the amount of transactions that are set on the state. Checks\n * if the length of the tx history is longer then desired persistence\n * limit and then if it is removes the oldest confirmed or rejected tx.\n * Pending or unapproved transactions will not be removed by this\n * operation. For safety of presenting a fully functional transaction UI\n * representation, this function will not break apart transactions with the\n * same nonce, created on the same day, per network. Not accounting for transactions of the same\n * nonce, same day and network combo can result in confusing or broken experiences\n * in the UI. The transactions are then updated using the BaseController update.\n *\n * @param transactions - The transactions to be applied to the state.\n * @returns The trimmed list of transactions.\n */\n private trimTransactionsForState(\n transactions: TransactionMeta[],\n ): TransactionMeta[] {\n const nonceNetworkSet = new Set();\n const txsToKeep = transactions.reverse().filter((tx) => {\n const { chainId, networkID, status, transaction, time } = tx;\n if (transaction) {\n const key = `${transaction.nonce}-${chainId ?? networkID}-${new Date(\n time,\n ).toDateString()}`;\n if (nonceNetworkSet.has(key)) {\n return true;\n } else if (\n nonceNetworkSet.size < this.config.txHistoryLimit ||\n !this.isFinalState(status)\n ) {\n nonceNetworkSet.add(key);\n return true;\n }\n }\n return false;\n });\n txsToKeep.reverse();\n return txsToKeep;\n }\n\n /**\n * Determines if the transaction is in a final state.\n *\n * @param status - The transaction status.\n * @returns Whether the transaction is in a final state.\n */\n private isFinalState(status: TransactionStatus): boolean {\n return (\n status === TransactionStatus.rejected ||\n status === TransactionStatus.confirmed ||\n status === TransactionStatus.failed ||\n status === TransactionStatus.cancelled\n );\n }\n\n /**\n * Method to verify the state of a transaction using the Blockchain as a source of truth.\n *\n * @param meta - The local transaction to verify on the blockchain.\n * @returns A tuple containing the updated transaction, and whether or not an update was required.\n */\n private async blockchainTransactionStateReconciler(\n meta: TransactionMeta,\n ): Promise<[TransactionMeta, boolean]> {\n const { status, transactionHash } = meta;\n switch (status) {\n case TransactionStatus.confirmed:\n const txReceipt = await query(this.ethQuery, 'getTransactionReceipt', [\n transactionHash,\n ]);\n\n if (!txReceipt) {\n return [meta, false];\n }\n\n meta.verifiedOnBlockchain = true;\n meta.transaction.gasUsed = txReceipt.gasUsed;\n\n // According to the Web3 docs:\n // TRUE if the transaction was successful, FALSE if the EVM reverted the transaction.\n if (Number(txReceipt.status) === 0) {\n const error: Error = new Error(\n 'Transaction failed. The transaction was reversed',\n );\n this.failTransaction(meta, error);\n return [meta, false];\n }\n\n return [meta, true];\n case TransactionStatus.submitted:\n const txObj = await query(this.ethQuery, 'getTransactionByHash', [\n transactionHash,\n ]);\n\n if (!txObj) {\n const receiptShowsFailedStatus =\n await this.checkTxReceiptStatusIsFailed(transactionHash);\n\n // Case the txObj is evaluated as false, a second check will\n // determine if the tx failed or it is pending or confirmed\n if (receiptShowsFailedStatus) {\n const error: Error = new Error(\n 'Transaction failed. The transaction was dropped or replaced by a new one',\n );\n this.failTransaction(meta, error);\n }\n }\n\n /* istanbul ignore next */\n if (txObj?.blockNumber) {\n meta.status = TransactionStatus.confirmed;\n this.hub.emit(`${meta.id}:confirmed`, meta);\n return [meta, true];\n }\n\n return [meta, false];\n default:\n return [meta, false];\n }\n }\n\n /**\n * Method to check if a tx has failed according to their receipt\n * According to the Web3 docs:\n * TRUE if the transaction was successful, FALSE if the EVM reverted the transaction.\n * The receipt is not available for pending transactions and returns null.\n *\n * @param txHash - The transaction hash.\n * @returns Whether the transaction has failed.\n */\n private async checkTxReceiptStatusIsFailed(\n txHash: string | undefined,\n ): Promise {\n const txReceipt = await query(this.ethQuery, 'getTransactionReceipt', [\n txHash,\n ]);\n if (!txReceipt) {\n // Transaction is pending\n return false;\n }\n return Number(txReceipt.status) === 0;\n }\n\n /**\n * Method to verify the state of transactions using Etherscan as a source of truth.\n *\n * @param remoteTxs - Transactions to reconcile that are from a remote source.\n * @param localTxs - Transactions to reconcile that are local.\n * @returns A tuple containing a boolean indicating whether or not an update was required, and the updated transaction.\n */\n private etherscanTransactionStateReconciler(\n remoteTxs: TransactionMeta[],\n localTxs: TransactionMeta[],\n ): [boolean, TransactionMeta[]] {\n const updatedTxs: TransactionMeta[] = this.getUpdatedTransactions(\n remoteTxs,\n localTxs,\n );\n\n const newTxs: TransactionMeta[] = this.getNewTransactions(\n remoteTxs,\n localTxs,\n );\n\n const updatedLocalTxs = localTxs.map((tx: TransactionMeta) => {\n const txIdx = updatedTxs.findIndex(\n ({ transactionHash }) => transactionHash === tx.transactionHash,\n );\n return txIdx === -1 ? tx : updatedTxs[txIdx];\n });\n\n const updateRequired = newTxs.length > 0 || updatedLocalTxs.length > 0;\n\n return [updateRequired, [...newTxs, ...updatedLocalTxs]];\n }\n\n /**\n * Get all transactions that are in the remote transactions array\n * but not in the local transactions array.\n *\n * @param remoteTxs - Array of transactions from remote source.\n * @param localTxs - Array of transactions stored locally.\n * @returns The new transactions.\n */\n private getNewTransactions(\n remoteTxs: TransactionMeta[],\n localTxs: TransactionMeta[],\n ): TransactionMeta[] {\n return remoteTxs.filter((tx) => {\n const alreadyInTransactions = localTxs.find(\n ({ transactionHash }) => transactionHash === tx.transactionHash,\n );\n return !alreadyInTransactions;\n });\n }\n\n /**\n * Get all the transactions that are locally outdated with respect\n * to a remote source (etherscan or blockchain). The returned array\n * contains the transactions with the updated data.\n *\n * @param remoteTxs - Array of transactions from remote source.\n * @param localTxs - Array of transactions stored locally.\n * @returns The updated transactions.\n */\n private getUpdatedTransactions(\n remoteTxs: TransactionMeta[],\n localTxs: TransactionMeta[],\n ): TransactionMeta[] {\n return remoteTxs.filter((remoteTx) => {\n const isTxOutdated = localTxs.find((localTx) => {\n return (\n remoteTx.transactionHash === localTx.transactionHash &&\n this.isTransactionOutdated(remoteTx, localTx)\n );\n });\n return isTxOutdated;\n });\n }\n\n /**\n * Verifies if a local transaction is outdated with respect to the remote transaction.\n *\n * @param remoteTx - The remote transaction from Etherscan.\n * @param localTx - The local transaction.\n * @returns Whether the transaction is outdated.\n */\n private isTransactionOutdated(\n remoteTx: TransactionMeta,\n localTx: TransactionMeta,\n ): boolean {\n const statusOutdated = this.isStatusOutdated(\n remoteTx.transactionHash,\n localTx.transactionHash,\n remoteTx.status,\n localTx.status,\n );\n const gasDataOutdated = this.isGasDataOutdated(\n remoteTx.transaction.gasUsed,\n localTx.transaction.gasUsed,\n );\n return statusOutdated || gasDataOutdated;\n }\n\n /**\n * Verifies if the status of a local transaction is outdated with respect to the remote transaction.\n *\n * @param remoteTxHash - Remote transaction hash.\n * @param localTxHash - Local transaction hash.\n * @param remoteTxStatus - Remote transaction status.\n * @param localTxStatus - Local transaction status.\n * @returns Whether the status is outdated.\n */\n private isStatusOutdated(\n remoteTxHash: string | undefined,\n localTxHash: string | undefined,\n remoteTxStatus: TransactionStatus,\n localTxStatus: TransactionStatus,\n ): boolean {\n return remoteTxHash === localTxHash && remoteTxStatus !== localTxStatus;\n }\n\n /**\n * Verifies if the gas data of a local transaction is outdated with respect to the remote transaction.\n *\n * @param remoteGasUsed - Remote gas used in the transaction.\n * @param localGasUsed - Local gas used in the transaction.\n * @returns Whether the gas data is outdated.\n */\n private isGasDataOutdated(\n remoteGasUsed: string | undefined,\n localGasUsed: string | undefined,\n ): boolean {\n return remoteGasUsed !== localGasUsed;\n }\n\n private async requestApproval(\n txMeta: TransactionMeta,\n { shouldShowRequest } = { shouldShowRequest: true },\n ) {\n const id = this.getApprovalId(txMeta);\n const { origin } = txMeta;\n const type = 'transaction';\n const requestData = { txId: txMeta.id };\n try {\n await this.messagingSystem.call(\n 'ApprovalController:addRequest',\n {\n id,\n origin: origin || 'metamask',\n type,\n requestData,\n },\n shouldShowRequest,\n );\n } catch (error) {\n console.info('Failed to request transaction approval', error);\n }\n }\n\n private acceptApproval(txMeta: TransactionMeta) {\n const id = this.getApprovalId(txMeta);\n try {\n this.messagingSystem.call('ApprovalController:acceptRequest', id);\n } catch (error) {\n console.info('Failed to accept transaction approval request', error);\n }\n }\n\n private rejectApproval(txMeta: TransactionMeta) {\n const id = this.getApprovalId(txMeta);\n try {\n this.messagingSystem.call(\n 'ApprovalController:rejectRequest',\n id,\n new Error('Rejected'),\n );\n } catch (error) {\n console.info('Failed to reject transaction approval request', error);\n }\n }\n\n private getApprovalId(txMeta: TransactionMeta) {\n return String(txMeta.id);\n }\n}\n\nexport default TransactionController;\n"]}
+\ No newline at end of file
diff --git a/sonar-project.properties b/sonar-project.properties
index 0eab66e3126..cffe2c959fa 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -14,5 +14,8 @@ sonar.sources=app/
# Inclusions for test files.
sonar.test.inclusions=**.test.**
+# Test coverage path
+sonar.javascript.lcov.reportPaths=/tests/coverage/lcov.info
+
# Encoding of the source code. Default is default system encoding
-sonar.sourceEncoding=UTF-8
+#sonar.sourceEncoding=UTF-8
diff --git a/storybook/storyLoader.js b/storybook/storyLoader.js
index 16b505d2cc9..dee71673af8 100644
--- a/storybook/storyLoader.js
+++ b/storybook/storyLoader.js
@@ -20,6 +20,8 @@ function loadStories() {
require('../app/component-library/components/Banners/Banner/Banner.stories');
require('../app/component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.stories');
require('../app/component-library/components/Banners/Banner/variants/BannerTip/BannerTip.stories');
+ require('../app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories');
+ require('../app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories');
require('../app/component-library/components/Buttons/Button/Button.stories');
require('../app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.stories');
require('../app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.stories');
@@ -38,6 +40,7 @@ function loadStories() {
require('../app/component-library/components/Form/TextFieldSearch/TextFieldSearch.stories');
require('../app/component-library/components/Header/Header.stories');
require('../app/component-library/components/Icons/Icon/Icon.stories');
+ require('../app/component-library/components/List/ListItem/ListItem.stories');
require('../app/component-library/components/Modals/ModalConfirmation/ModalConfirmation.stories');
require('../app/component-library/components/Modals/ModalMandatory/ModalMandatory.stories');
require('../app/component-library/components/Navigation/TabBar/TabBar.stories');
@@ -86,6 +89,8 @@ const stories = [
'../app/component-library/components/Banners/Banner/Banner.stories',
'../app/component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.stories',
'../app/component-library/components/Banners/Banner/variants/BannerTip/BannerTip.stories',
+ '../app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories',
+ '../app/component-library/components/BottomSheets/BottomSheetHeader/BottomSheetHeader.stories',
'../app/component-library/components/Buttons/Button/Button.stories',
'../app/component-library/components/Buttons/Button/foundation/ButtonBase/ButtonBase.stories',
'../app/component-library/components/Buttons/Button/variants/ButtonLink/ButtonLink.stories',
@@ -104,6 +109,7 @@ const stories = [
'../app/component-library/components/Form/TextFieldSearch/TextFieldSearch.stories',
'../app/component-library/components/Header/Header.stories',
'../app/component-library/components/Icons/Icon/Icon.stories',
+ '../app/component-library/components/List/ListItem/ListItem.stories',
'../app/component-library/components/Modals/ModalConfirmation/ModalConfirmation.stories',
'../app/component-library/components/Modals/ModalMandatory/ModalMandatory.stories',
'../app/component-library/components/Navigation/TabBar/TabBar.stories',
diff --git a/wdio.conf.js b/wdio.conf.js
index d7a3d6e9740..a5d482f2584 100644
--- a/wdio.conf.js
+++ b/wdio.conf.js
@@ -3,7 +3,13 @@ dotenv.config({ path: '.e2e.env' });
import generateTestReports from './wdio/utils/generateTestReports';
import ADB from 'appium-adb';
-
+import { gasApiDown, cleanAllMocks } from './wdio/utils/mocks';
+import {
+ startGanache,
+ stopGanache,
+ deployMultisig,
+ deployErc20,
+} from './wdio/utils/ganache';
const { removeSync } = require('fs-extra');
export const config = {
@@ -33,12 +39,15 @@ export const config = {
specs: ['./wdio/features/**/*.feature'],
suites: {
- confirmations: ['./wdio/features/Confirmations/*.feature']
+ confirmations: ['./wdio/features/Confirmations/*.feature'],
},
-
+
// Patterns to exclude.
exclude: [
- // 'path/to/excluded/files'
+ './wdio/features/Wallet/AddressFlow.feature',
+ './wdio/features/Wallet/ImportCustomToken.feature',
+ './wdio/features/Wallet/SendToken.feature',
+ './wdio/features/Accounts/AccountActions.feature'
],
//
// ============
@@ -295,7 +304,26 @@ export const config = {
* @param {ITestCaseHookParameter} world world object containing information on pickle and test step
* @param {Object} context Cucumber World object
*/
- beforeScenario: async function (world, context) {},
+ beforeScenario: async function (world, context) {
+ const tags = world.pickle.tags;
+
+ if (tags.filter((e) => e.name === '@ganache').length > 0) {
+ await startGanache();
+ }
+
+ if (tags.filter((e) => e.name === '@multisig').length > 0) {
+ const multisig = await deployMultisig();
+ context.multisig = multisig;
+ }
+
+ if (tags.filter((e) => e.name === '@erc20').length > 0) {
+ context.erc20 = await deployErc20();
+ }
+
+ if (tags.filter((e) => e.name === '@gasApiDown').length > 0) {
+ context.mock = gasApiDown();
+ }
+ },
/**
*
* Runs before a Cucumber Step.
@@ -328,7 +356,17 @@ export const config = {
* @param {number} result.duration duration of scenario in milliseconds
* @param {Object} context Cucumber World object
*/
- afterScenario: async function (world, result, context) {},
+ afterScenario: async function (world, context) {
+ const tags = world.pickle.tags;
+
+ if (tags.filter((e) => e.name === '@ganache').length > 0) {
+ await stopGanache();
+ }
+
+ if (tags.filter((e) => e.name === '@mock').length > 0) {
+ cleanAllMocks();
+ }
+ },
/**
*
* Runs after a Cucumber Feature.
diff --git a/wdio/features/Accounts/CreatingWalletAccount.feature b/wdio/features/Accounts/CreatingWalletAccount.feature
index 9fe4646afa4..dab04777e9b 100644
--- a/wdio/features/Accounts/CreatingWalletAccount.feature
+++ b/wdio/features/Accounts/CreatingWalletAccount.feature
@@ -13,5 +13,5 @@ Feature: Create Account
Given I am on the wallet view
When I tap on the Identicon
Then the account list should be visible
- When I tap on Create a new account
+ When I tap Create a new account
Then I am on the new account
diff --git a/wdio/features/Accounts/ImportingAccount.feature b/wdio/features/Accounts/ImportingAccount.feature
index 7b3f35884a9..53d46b442a1 100644
--- a/wdio/features/Accounts/ImportingAccount.feature
+++ b/wdio/features/Accounts/ImportingAccount.feature
@@ -1,7 +1,7 @@
@androidApp
@regression
@accounts
-Feature: Import Aaccount
+Feature: Import Account
Scenario: Import Wallet
Given the app displayed the splash animation
@@ -14,7 +14,7 @@ Feature: Import Aaccount
Given I am on the wallet view
When I tap on the Identicon
Then the account list should be visible
- When I tap on Import an account
+ When I tap import account
Then I am taken to the Import Account screen
When I type into the private key input field
And I tap on the private key import button
@@ -30,7 +30,7 @@ Feature: Import Aaccount
Given I am on the wallet view
When I tap on the Identicon
Then the account list should be visible
- When I tap on Import an account
+ When I tap import account
Then I am taken to the Import Account screen
When I type into the private key input field
And I tap on the private key import button
diff --git a/wdio/features/BrowserFlow/RemovingImportedAccountAfterConnectingToDapp.feature b/wdio/features/BrowserFlow/RemovingImportedAccountAfterConnectingToDapp.feature
index 8aae1fa53f3..b51b1090045 100644
--- a/wdio/features/BrowserFlow/RemovingImportedAccountAfterConnectingToDapp.feature
+++ b/wdio/features/BrowserFlow/RemovingImportedAccountAfterConnectingToDapp.feature
@@ -26,8 +26,8 @@ Feature: Browser Import, Revoke, Remove Account
When I trigger the connect modal
Then the connect modal should be displayed
When I tap on button with text "Connect multiple accounts"
- And I tap on button with text "Import an account"
- When I type into the private key input field
+ And I tap import account
+ And I type into the private key input field
And I tap on the private key import button
Then The account is imported
When I tap on Select all button
diff --git a/wdio/features/Confirmations/ApproveCustomERC20.feature b/wdio/features/Confirmations/ApproveCustomERC20.feature
new file mode 100644
index 00000000000..2f67cbdc60c
--- /dev/null
+++ b/wdio/features/Confirmations/ApproveCustomERC20.feature
@@ -0,0 +1,21 @@
+@androidApp
+@confirmations
+@regression
+
+Feature: Approve an ERC20 token with custom amount
+ A user should be able to approve an ERC20 token from a dapp setting a custom amount.
+ @ganache
+ @erc20
+ Scenario: should approve successfully the custom token amount I set
+ Given the app displayed the splash animation
+ And I have imported my wallet
+ And I tap No Thanks on the Enable security check screen
+ And I tap No thanks on the onboarding welcome tutorial
+ And Ganache network is selected
+ When I navigate to the browser
+ And I am on Home MetaMask website
+ And I navigate to "test-dapp-erc20"
+ And I connect my active wallet to the test dapp
+ And I scroll to the ERC20 section
+ And I approve the custom ERC20 token amount
+ Then the transaction is submitted with Transaction Complete! toast appearing
\ No newline at end of file
diff --git a/wdio/features/Confirmations/ApproveDefaultERC20.feature b/wdio/features/Confirmations/ApproveDefaultERC20.feature
new file mode 100644
index 00000000000..bcd389b221a
--- /dev/null
+++ b/wdio/features/Confirmations/ApproveDefaultERC20.feature
@@ -0,0 +1,21 @@
+@androidApp
+@confirmations
+@regression
+
+Feature: Approve an ERC20 token with default dapp suggested amount
+ A user should be able to approve an ERC20 token from a dapp using the default dapp suggested amount.
+ @ganache
+ @erc20
+ Scenario: should approve successfully the default token amount suggested by the dapp
+ Given the app displayed the splash animation
+ And I have imported my wallet
+ And I tap No Thanks on the Enable security check screen
+ And I tap No thanks on the onboarding welcome tutorial
+ And Ganache network is selected
+ When I navigate to the browser
+ And I am on Home MetaMask website
+ And I navigate to "test-dapp-erc20"
+ And I connect my active wallet to the test dapp
+ And I scroll to the ERC20 section
+ And I approve default ERC20 token amount
+ Then the transaction is submitted with Transaction Complete! toast appearing
\ No newline at end of file
diff --git a/wdio/features/Confirmations/DappSendERC20.feature b/wdio/features/Confirmations/SendERC20.feature
similarity index 65%
rename from wdio/features/Confirmations/DappSendERC20.feature
rename to wdio/features/Confirmations/SendERC20.feature
index de3865dbe8a..b6911a6cde0 100644
--- a/wdio/features/Confirmations/DappSendERC20.feature
+++ b/wdio/features/Confirmations/SendERC20.feature
@@ -2,24 +2,20 @@
@confirmations
@regression
-Feature: Sending an ERC20 token from a dapp
- User should be able to send an ERC20 token from a dapp.
-
- Scenario: Import wallet
+Feature: Send an ERC20 token
+ A user should be able to send an ERC20.
+ @ganache
+ @erc20
+ Scenario: should successfully send an ERC20 token from a dapp
Given the app displayed the splash animation
And I have imported my wallet
And I tap No Thanks on the Enable security check screen
And I tap No thanks on the onboarding welcome tutorial
-
- Scenario: Send ERC20 token from a dapp
- Given Ganache server is started
And Ganache network is selected
- And ERC20 token contract is deployed
When I navigate to the browser
And I am on Home MetaMask website
When I navigate to "test-dapp-erc20"
And I connect my active wallet to the test dapp
And I scroll to the ERC20 section
And I transfer ERC20 tokens
- Then the transaction is submitted with Transaction Complete! toast appearing
- Then Ganache server is stopped
\ No newline at end of file
+ Then the transaction is submitted with Transaction Complete! toast appearing
\ No newline at end of file
diff --git a/wdio/features/Confirmations/SendEthEOA.feature b/wdio/features/Confirmations/SendEthEOA.feature
index f499a12ce9b..c0f97200476 100644
--- a/wdio/features/Confirmations/SendEthEOA.feature
+++ b/wdio/features/Confirmations/SendEthEOA.feature
@@ -2,31 +2,23 @@
@confirmations
@regression
-Feature: Sending ETH to an EOA
- User should be able to send ETH to another EOA address.
-
- Scenario: Import wallet
+Feature: Send ETH to an EOA
+ A user should be able to send ETH to another EOA address.
+ @ganache
+ Scenario: should successfully send ETH to an EOA from inside MetaMask wallet
Given the app displayed the splash animation
And I have imported my wallet
And I tap No Thanks on the Enable security check screen
And I tap No thanks on the onboarding welcome tutorial
-
- Scenario Outline: Sending ETH to an EOA from inside MetaMask wallet
- Given Ganache server is started
And Ganache network is selected
- When On the Main Wallet view I tap "ETHER"
- And On the Main Wallet view I tap "Send"
- And I enter address "" in the sender's input box
- When I tap button "Next" on Send To view
+ When On the Main Wallet view I tap on the Send Action
+ And I enter address "0x1FDb169Ef12954F20A15852980e1F0C122BfC1D6" in the sender's input box
+ And I tap button "Next" on Send To view
Then I proceed to the amount view
- When I type amount "" into amount input field
+ When I type amount "1" into amount input field
And I tap button "Next" on the Amount view
Then I should be taken to the transaction confirmation view
- And the token amount to be sent is visible
+ And the token amount 1 to be sent is visible
When I tap button "Send" on Confirm Amount view
- Then I am taken to the token overview screen
- And the transaction is submitted with Transaction Complete! toast appearing
- Then Ganache server is stopped
- Examples:
- | Address | Amount |
- | 0x1FDb169Ef12954F20A15852980e1F0C122BfC1D6 | 1 |
+ Then I am on the main wallet view
+ And the transaction is submitted with Transaction Complete! toast appearing
\ No newline at end of file
diff --git a/wdio/features/Confirmations/SendEthGasApiDown.feature b/wdio/features/Confirmations/SendEthGasApiDown.feature
new file mode 100644
index 00000000000..2c2ddd89837
--- /dev/null
+++ b/wdio/features/Confirmations/SendEthGasApiDown.feature
@@ -0,0 +1,29 @@
+@androidApp
+@confirmations
+@regression
+
+Feature: Send ETH with Gas API down
+ A user should be able to send ETH when the Gas API is down.
+ @ganache
+ @mock
+ @gasApiDown
+ Scenario: should display fallback gas properties on the Gas Edit screen
+ Given the app displayed the splash animation
+ And I have imported my wallet
+ And I tap No Thanks on the Enable security check screen
+ And I tap No thanks on the onboarding welcome tutorial
+ And Ganache network is selected
+ When On the Main Wallet view I tap on the Send Action
+ And I enter address "0x1FDb169Ef12954F20A15852980e1F0C122BfC1D6" in the sender's input box
+ And I tap button "Next" on Send To view
+ Then I proceed to the amount view
+ When I type amount "1" into amount input field
+ And I tap button "Next" on the Amount view
+ Then I should be taken to the transaction confirmation view
+ And the token amount 1 to be sent is visible
+ When I tap Edit Gas link
+ Then suggested gas options should not be visible
+ When I tap Save Gas Values
+ When I tap button "Send" on Confirm Amount view
+ Then I am on the main wallet view
+ And the transaction is submitted with Transaction Complete! toast appearing
\ No newline at end of file
diff --git a/wdio/features/Confirmations/SendEthMultisig.feature b/wdio/features/Confirmations/SendEthMultisig.feature
index 593031e9ebf..4d6eef0ff70 100644
--- a/wdio/features/Confirmations/SendEthMultisig.feature
+++ b/wdio/features/Confirmations/SendEthMultisig.feature
@@ -2,32 +2,24 @@
@confirmations
@regression
-Feature: Sending ETH to a Multisig
- User should be able to send ETH to a Multisig address.
-
- Scenario: Import wallet
+Feature: Send ETH to a Multisig
+ A user should be able to send ETH to a Multisig address.
+ @ganache
+ @multisig
+ Scenario: should successfully send ETH to a Multisig address from inside MetaMask wallet
Given the app displayed the splash animation
And I have imported my wallet
And I tap No Thanks on the Enable security check screen
And I tap No thanks on the onboarding welcome tutorial
-
- Scenario Outline: Sending ETH to a Multisig address from inside MetaMask wallet
- Given Ganache server is started
And Ganache network is selected
- And Multisig contract is deployed
- When On the Main Wallet view I tap "ETHER"
- And On the Main Wallet view I tap "Send"
+ When On the Main Wallet view I tap on the Send Action
And I enter address "MultisigAddress" in the sender's input box
When I tap button "Next" on Send To view
Then I proceed to the amount view
- When I type amount "" into amount input field
+ When I type amount "1" into amount input field
And I tap button "Next" on the Amount view
Then I should be taken to the transaction confirmation view
- And the token amount to be sent is visible
+ And the token amount 1 to be sent is visible
When I tap button "Send" on Confirm Amount view
- Then I am taken to the token overview screen
- And the transaction is submitted with Transaction Complete! toast appearing
- Then Ganache server is stopped
- Examples:
- | Amount |
- | 1 |
\ No newline at end of file
+ Then I am on the main wallet view
+ And the transaction is submitted with Transaction Complete! toast appearing
\ No newline at end of file
diff --git a/wdio/features/Networks/NetworkFlow.feature b/wdio/features/Networks/NetworkFlow.feature
index 188961d4ebd..1cafcd6379d 100644
--- a/wdio/features/Networks/NetworkFlow.feature
+++ b/wdio/features/Networks/NetworkFlow.feature
@@ -23,9 +23,7 @@ User should also have the ability to a add custom network via the custom network
When I tap on Switch network
Then "" should be displayed in network educational modal
And I should see the added network name "" in the top navigation bar
- # And my token balance shows up correctly with token "ll"
- When I tap on the burger menu
- And I tap on "Settings" in the menu
+ When I tap on the Settings tab option
And In settings I tap on "Networks"
Then "" should be visible below the Custom Networks section
When I tap on the Add Network button
@@ -44,7 +42,7 @@ User should also have the ability to a add custom network via the custom network
And I type "" into the RPC url field
And I type "" into the Chain ID field
And I type "" into the Network symbol field
- And I tap on the Add button
+ And I tap on the Add button to add Custom Network
Then "" should be displayed in network educational modal
And I should see the added network name "" in the top navigation bar
Examples:
@@ -52,8 +50,7 @@ User should also have the ability to a add custom network via the custom network
| Gnosis | https://xdai-rpc.gateway.pokt.network | 100 | xDAI |
Scenario Outline: I can remove a custom network that was added via the popular network flow
- Given I tap on the burger menu
- And I tap on "Settings" in the menu
+ Given I tap on the Settings tab option
And In settings I tap on "Networks"
And the network screen is displayed
And I tap on the Add Network button
@@ -67,8 +64,7 @@ User should also have the ability to a add custom network via the custom network
When I tap on Switch network
Then "" should be displayed in network educational modal
Then I should see the added network name "" in the top navigation bar
- When I tap on the burger menu
- And I tap on "Settings" in the menu
+ When I tap on the Settings tab option
And In settings I tap on "Networks"
And I tap and hold network ""
And I click "Delete" on remove network modal
@@ -88,11 +84,10 @@ User should also have the ability to a add custom network via the custom network
And I type "" into the RPC url field
And I type "" into the Chain ID field
And I type "" into the Network symbol field
- And I tap on the Add button
+ And I tap on the Add button to add Custom Network
Then "" should be displayed in network educational modal
Then I should see the added network name "" in the top navigation bar
- When I tap on the burger menu
- And I tap on "Settings" in the menu
+ When I tap on the Settings tab option
And In settings I tap on "Networks"
And the network screen is displayed
And I tap on network "" on networks screen
diff --git a/wdio/features/Onboarding/ImportWallet.feature b/wdio/features/Onboarding/ImportWallet.feature
index 8651edf4426..fe043de777c 100644
--- a/wdio/features/Onboarding/ImportWallet.feature
+++ b/wdio/features/Onboarding/ImportWallet.feature
@@ -47,7 +47,9 @@ Feature: Onboarding Import Wallet
And I type in confirm password field
And I tap "Import"
And I tap No Thanks on the Enable security check screen
- Then "Welcome to your new wallet!" is displayed
+ And I tap No thanks on the onboarding welcome tutorial
+ And I close the Whats New modal
+ Then I am on the main wallet view
Examples:
| SRP | password |
| fold media south add since false relax immense pause cloth just raven | Metapass12345!@ |
diff --git a/wdio/features/SecurityAndPrivacy/DeleteWallet.feature b/wdio/features/SecurityAndPrivacy/DeleteWallet.feature
index 148a1bf80e1..6aa4793a24e 100644
--- a/wdio/features/SecurityAndPrivacy/DeleteWallet.feature
+++ b/wdio/features/SecurityAndPrivacy/DeleteWallet.feature
@@ -10,8 +10,7 @@ Feature: Security & Privacy Delete Wallet
And I tap No thanks on the onboarding welcome tutorial
Scenario: Delete wallet from Settings
- When I tap burger icon
- And I tap on "Settings" in the menu
+ When I tap on the Settings tab option
And In settings I tap on "Security & Privacy"
Then Security & Privacy screen is displayed
When I tap on the Delete Wallet button
diff --git a/wdio/features/SecurityAndPrivacy/RememberMe.feature b/wdio/features/SecurityAndPrivacy/RememberMe.feature
index a5161f9d926..114eb7736bb 100644
--- a/wdio/features/SecurityAndPrivacy/RememberMe.feature
+++ b/wdio/features/SecurityAndPrivacy/RememberMe.feature
@@ -8,8 +8,8 @@ Feature: Security & Privacy Remember Me
And I have imported my wallet
And I tap No Thanks on the Enable security check screen
And I tap No thanks on the onboarding welcome tutorial
- When I tap burger icon
- And I tap on "Settings" in the menu
+ And I close the Whats New modal
+ When I tap on the Settings tab option
And In settings I tap on "Security & Privacy"
Then on Security & Privacy screen I toggle on Remember me
When I kill the app
diff --git a/wdio/features/Settings/ChangePassword.feature b/wdio/features/Settings/ChangePassword.feature
index 4bab9cf60a9..9c1b46a0e52 100644
--- a/wdio/features/Settings/ChangePassword.feature
+++ b/wdio/features/Settings/ChangePassword.feature
@@ -10,8 +10,7 @@ Feature: Settings Change Password
And I tap No thanks on the onboarding welcome tutorial
Scenario: Navigate to Change Password in Settings
- When I tap burger icon
- And I tap on "Settings" in the menu
+ When I tap on the Settings tab option
And In settings I tap on "Security & Privacy"
Then on Security & Privacy screen I tap "Change password"
diff --git a/wdio/features/Wallet/AddressFlow.feature b/wdio/features/Wallet/AddressFlow.feature
index b02afc32b38..d1a7dbddb9c 100644
--- a/wdio/features/Wallet/AddressFlow.feature
+++ b/wdio/features/Wallet/AddressFlow.feature
@@ -36,8 +36,7 @@ Feature: Add Contacts
When I tap the Save button
And the contact name "" appears in the senders input box above the contact address
And I navigate to the main wallet screen
- And I tap burger icon
- And I tap on "Settings" in the menu
+ And I tap on the Settings tab option
And In settings I tap on "Contacts"
Then the saved contact "" should appear
Examples:
@@ -79,8 +78,7 @@ Feature: Add Contacts
And I tap on button with text "Goerli Test Network"
And I tap on button with text "Got it"
Then I should see the added network name "Goerli Test Network" in the top navigation bar
- And I tap burger icon
- And I tap on "Settings" in the menu
+ And I tap on the Settings tab option
And In settings I tap on "Contacts"
Then I should not see "" appear in the contact list
Examples:
diff --git a/wdio/features/Wallet/ExploringWizard.feature b/wdio/features/Wallet/ExploringWizard.feature
index d38f604d7e9..86ef77daed7 100644
--- a/wdio/features/Wallet/ExploringWizard.feature
+++ b/wdio/features/Wallet/ExploringWizard.feature
@@ -9,25 +9,25 @@ Feature: Exploring wizard
And I tap No Thanks on the Enable security check screen
And the onboarding wizard is visible on wallet view
When On the onboarding wizard I tap on "Take a Tour" button
- Then the tutorial modal heading should read "Your Accounts"
+ Then the tutorial modal heading should read "Your accounts"
And there should be an explanation of the accounts functionality.
When On the onboarding wizard I tap on "Got it" button
- Then the tutorial modal heading should read "Edit Account Name"
+ Then the tutorial modal heading should read "Managing your account"
And there should be an explanation about adding a nickname to your account.
When On the onboarding wizard I tap on "Got it" button
- Then the tutorial modal heading should read "Main Menu"
+ Then the tutorial modal heading should read "Using your wallet"
And there should be an explanation of the what exists within the main menu.
When On the onboarding wizard I tap on "Got it" button
- Then the tutorial modal heading should read "Explore the Browser"
+ Then the tutorial modal heading should read "Exploring web3"
And there should be an explanation of the what the purpose of the browser.
When On the onboarding wizard I tap on "Back" button
- Then the tutorial modal heading should read "Main Menu"
+ Then the tutorial modal heading should read "Using your wallet"
And there should be an explanation of the what exists within the main menu.
When On the onboarding wizard I tap on "Got it" button
- Then the tutorial modal heading should read "Explore the Browser"
+ Then the tutorial modal heading should read "Exploring web3"
And there should be an explanation of the what the purpose of the browser.
When On the onboarding wizard I tap on "Got it" button
- Then the tutorial modal heading should read "Search"
+ Then the tutorial modal heading should read "Using the browser"
And there should be an explanation of the what the purpose of the search input box.
When On the onboarding wizard I tap on "Got it" button
Then the onboarding wizard is no longer visible
diff --git a/wdio/features/Wallet/ImportCustomToken.feature b/wdio/features/Wallet/ImportCustomToken.feature
index 845c8a8058e..7c3197b5b05 100644
--- a/wdio/features/Wallet/ImportCustomToken.feature
+++ b/wdio/features/Wallet/ImportCustomToken.feature
@@ -16,7 +16,7 @@ Feature: Import Custom Token
And I type "" into the RPC url field
And I type "" into the Chain ID field
And I type "" into the Network symbol field
- When I tap on the Add button
+ When I tap on the Add button to add Custom Network
Then "" should be displayed in network educational modal
And I should see the added network name "" in the top navigation bar
Examples:
diff --git a/wdio/features/Wallet/LockResetWallet.feature b/wdio/features/Wallet/LockResetWallet.feature
index 498aa20df39..bafde200898 100644
--- a/wdio/features/Wallet/LockResetWallet.feature
+++ b/wdio/features/Wallet/LockResetWallet.feature
@@ -10,8 +10,8 @@ Feature: Lock and Reset Wallet
And I tap No thanks on the onboarding welcome tutorial
Scenario Outline: Lock Wallet
- When I tap burger icon
- And I tap Lock menu item
+ When I tap on the Settings tab option
+ And In settings I tap on the Lock Option
Then device alert is displayed
When I tap Yes on alert
Then Login screen is displayed
diff --git a/wdio/features/Wallet/RequestTokenFlow.feature b/wdio/features/Wallet/RequestTokenFlow.feature
index f13b8d0a26e..2876776cb6d 100644
--- a/wdio/features/Wallet/RequestTokenFlow.feature
+++ b/wdio/features/Wallet/RequestTokenFlow.feature
@@ -20,6 +20,7 @@ This feature goes through the request token flow
Then the network approval modal has button "Switch Network" displayed
When I tap on button with text "Close"
And I close the networks screen view
+ And I navigate to the wallet
Then I am on the main wallet view
Scenario Outline: Request native token
@@ -70,4 +71,4 @@ This feature goes through the request token flow
Examples:
| Network | TokenID | FirstTokenName | SecondTokenID | SecondTokenName |
| BNB Smart Chain | BETH | Binance Beacon ETH | Link | ChainLink Token |
- | Ethereum Main Network | QNT | Quant | CETH | Compound Ether |
+ | Ethereum Main Network | QNT | Quant Network | CETH | Compound Ether |
diff --git a/wdio/features/Wallet/SendToken.feature b/wdio/features/Wallet/SendToken.feature
index 6c746cf8952..c4d893b2529 100644
--- a/wdio/features/Wallet/SendToken.feature
+++ b/wdio/features/Wallet/SendToken.feature
@@ -26,7 +26,7 @@ Feature: Sending Native and ERC Tokens
Scenario Outline: A user can send ERC-20 tokens to an Address via token overview screen
Given I am on the wallet view
- When I tap Token containing text ""
+ When I tap on button with text ""
Then I am taken to the token overview screen
When I tap button Send on Token screen view
And I enter address "" in the sender's input box
diff --git a/wdio/screen-objects/AccountListComponent.js b/wdio/screen-objects/AccountListComponent.js
index e18539f9ce3..d87631ddfa4 100644
--- a/wdio/screen-objects/AccountListComponent.js
+++ b/wdio/screen-objects/AccountListComponent.js
@@ -1,9 +1,8 @@
import Gestures from '../helpers/Gestures';
import Selectors from '../helpers/Selectors';
import {
+ ACCOUNT_LIST_ADD_BUTTON_ID,
ACCOUNT_LIST_ID,
- CREATE_ACCOUNT_BUTTON_ID,
- IMPORT_ACCOUNT_BUTTON_ID,
} from './testIDs/Components/AccountListComponent.testIds';
class AccountListComponent {
@@ -11,20 +10,12 @@ class AccountListComponent {
return Selectors.getElementByPlatform(ACCOUNT_LIST_ID);
}
- get createAccountButton() {
- return Selectors.getElementByPlatform(CREATE_ACCOUNT_BUTTON_ID);
+ get addAccountButton() {
+ return Selectors.getElementByPlatform(ACCOUNT_LIST_ADD_BUTTON_ID);
}
- get importAccountButton() {
- return Selectors.getElementByPlatform(IMPORT_ACCOUNT_BUTTON_ID);
- }
-
- async tapCreateAccountButton() {
- await Gestures.waitAndTap(this.createAccountButton);
- }
-
- async tapImportAccountButton() {
- await Gestures.waitAndTap(this.importAccountButton);
+ async tapAddAccountButton() {
+ await Gestures.waitAndTap(this.addAccountButton);
}
async isComponentDisplayed() {
diff --git a/wdio/screen-objects/AddCustomImportTokensScreen.js b/wdio/screen-objects/AddCustomImportTokensScreen.js
index c494a11d8f5..69a63fc40eb 100644
--- a/wdio/screen-objects/AddCustomImportTokensScreen.js
+++ b/wdio/screen-objects/AddCustomImportTokensScreen.js
@@ -29,16 +29,7 @@ class AddCustomImportToken {
}
async tapImportButton() {
- const importButton = await this.importButton;
- let displayed = true;
- while (displayed) {
- if (await importButton.isExisting()) {
- await importButton.click();
- await driver.pause(3000);
- } else {
- displayed = false;
- }
- }
+ await Gestures.waitAndTap(this.importButton);
}
async tapTokenSymbolField() {
diff --git a/wdio/screen-objects/BrowserObject/ExternalWebsitesScreen.js b/wdio/screen-objects/BrowserObject/ExternalWebsitesScreen.js
index dbb907c7f33..3642df1b141 100644
--- a/wdio/screen-objects/BrowserObject/ExternalWebsitesScreen.js
+++ b/wdio/screen-objects/BrowserObject/ExternalWebsitesScreen.js
@@ -81,6 +81,10 @@ class ExternalWebsitesScreen {
return Selectors.getXpathElementByText('TRANSFER TOKENS');
}
+ get testDappApproveTokens() {
+ return Selectors.getXpathElementByText('APPROVE TOKENS');
+ }
+
async tapHomeFavoritesButton() {
const element = await this.homeFavoriteButton;
await element.waitForEnabled();
@@ -136,6 +140,12 @@ class ExternalWebsitesScreen {
await Gestures.waitAndTap(this.testDappTransferTokens);
}
+ async tapDappApproveTokens() {
+ const element = await this.testDappApproveTokens;
+ await element.waitForEnabled();
+ await Gestures.waitAndTap(this.testDappApproveTokens);
+ }
+
async tapUniswapMetaMaskWalletButton() {
await Gestures.tapTextByXpath('MetaMask');
}
diff --git a/wdio/screen-objects/Modals/AccountApprovalModal.js b/wdio/screen-objects/Modals/AccountApprovalModal.js
index 1d05d77b373..50bb413ee42 100644
--- a/wdio/screen-objects/Modals/AccountApprovalModal.js
+++ b/wdio/screen-objects/Modals/AccountApprovalModal.js
@@ -27,6 +27,14 @@ class AccountApprovalModal {
return Selectors.getElementByPlatform(ACCOUNT_APPROVAL_SELECT_ALL_BUTTON);
}
+ get amountInputField() {
+ return Selectors.getXpathElementByText('Enter a number here');
+ }
+
+ get nextButton() {
+ return Selectors.getXpathElementByText('Next');
+ }
+
async tapConnectButton() {
await Gestures.waitAndTap(this.connectButton);
}
@@ -45,6 +53,22 @@ class AccountApprovalModal {
await Gestures.tapTextByXpath('Confirm'); // needed for browser specific tests
}
+ async tapUseDefaultApproveByText() {
+ await Gestures.tapTextByXpath('Use default'); // needed for browser specific tests
+ }
+
+ async setTokenAmount(amount) {
+ await Gestures.typeText(this.amountInputField, amount);
+ }
+
+ async tapNextButtonByText() {
+ await Gestures.waitAndTap(this.nextButton);
+ }
+
+ async tapApproveButtonByText() {
+ await Gestures.tapTextByXpath('Approve'); // needed for browser specific tests
+ }
+
async isVisible() {
const modalContainer = await this.modalContainer;
await modalContainer.waitForDisplayed();
diff --git a/wdio/screen-objects/Modals/AddAccountModal.js b/wdio/screen-objects/Modals/AddAccountModal.js
new file mode 100644
index 00000000000..d4697bdd7b0
--- /dev/null
+++ b/wdio/screen-objects/Modals/AddAccountModal.js
@@ -0,0 +1,28 @@
+import Selectors from '../../helpers/Selectors';
+import {
+ ADD_ACCOUNT_IMPORT_ACCOUNT_BUTTON,
+ ADD_ACCOUNT_NEW_ACCOUNT_BUTTON,
+} from '../testIDs/Components/AddAccountModal.testIds';
+import Gestures from '../../helpers/Gestures';
+
+class AddAccountModal {
+ get newAccountButton() {
+ return Selectors.getElementByPlatform(ADD_ACCOUNT_NEW_ACCOUNT_BUTTON);
+ }
+
+ get importAccountButton() {
+ return Selectors.getElementByPlatform(ADD_ACCOUNT_IMPORT_ACCOUNT_BUTTON);
+ }
+
+ async tapNewAccountButton() {
+ await Gestures.waitAndTap(this.newAccountButton);
+ const newAccountButton = await this.newAccountButton;
+ await newAccountButton.waitForExist({ reverse: true });
+ }
+
+ async tapImportAccountButton() {
+ await Gestures.waitAndTap(this.importAccountButton);
+ }
+}
+
+export default new AddAccountModal();
diff --git a/wdio/screen-objects/Modals/AddressBookModal.js b/wdio/screen-objects/Modals/AddressBookModal.js
index f96822acbe2..fe54ccd3a94 100644
--- a/wdio/screen-objects/Modals/AddressBookModal.js
+++ b/wdio/screen-objects/Modals/AddressBookModal.js
@@ -1,6 +1,10 @@
import Gestures from '../../helpers/Gestures';
import Selectors from '../../helpers/Selectors';
-import { ENTER_ALIAS_INPUT_BOX_ID } from '../testIDs/Screens/AddressBook.testids';
+import {
+ ADDRESS_ALIAS_SAVE_BUTTON_ID,
+ ADDRESS_ALIAS_TITLE_ID,
+ ENTER_ALIAS_INPUT_BOX_ID
+} from '../testIDs/Screens/AddressBook.testids';
import { ADD_ADDRESS_MODAL_CONTAINER_ID } from '../../../app/constants/test-ids';
class AddressBookModal {
@@ -12,6 +16,18 @@ class AddressBookModal {
return Selectors.getElementByPlatform(ENTER_ALIAS_INPUT_BOX_ID);
}
+ get saveButton() {
+ return Selectors.getElementByPlatform(ADDRESS_ALIAS_SAVE_BUTTON_ID);
+ }
+
+ get cancelButton() {
+ return Selectors.getElementByPlatform(ADDRESS_ALIAS_SAVE_BUTTON_ID);
+ }
+
+ get title() {
+ return Selectors.getElementByPlatform(ADDRESS_ALIAS_TITLE_ID);
+ }
+
async waitForDisplayed() {
const container = await this.container;
await container.waitForDisplayed();
@@ -22,15 +38,19 @@ class AddressBookModal {
}
async isCancelButtonEnabled() {
- expect(await Selectors.getXpathElementByText('Cancel')).toBeEnabled();
+ expect(this.cancelButton).toBeEnabled();
}
async isSaveButtonEnabled() {
- expect(await Selectors.getXpathElementByText('Save')).toBeEnabled();
+ expect(this.saveButton).toBeEnabled();
}
async tapOnSaveButton() {
- await Gestures.tap(await Selectors.getXpathElementByText('Save'));
+ await Gestures.waitAndTap(this.saveButton);
+ }
+
+ async tapTitle() {
+ await Gestures.waitAndTap(this.title);
}
async isContactNameVisible(contact) {
diff --git a/wdio/screen-objects/Modals/TabBarModal.js b/wdio/screen-objects/Modals/TabBarModal.js
index f66dc5bb2a2..5a94bcb3383 100644
--- a/wdio/screen-objects/Modals/TabBarModal.js
+++ b/wdio/screen-objects/Modals/TabBarModal.js
@@ -2,6 +2,7 @@ import Selectors from '../../helpers/Selectors';
import {
TAB_BAR_ACTION_BUTTON,
TAB_BAR_BROWSER_BUTTON,
+ TAB_BAR_SETTING_BUTTON,
TAB_BAR_WALLET_BUTTON,
} from '../testIDs/Components/TabBar.testIds';
import Gestures from '../../helpers/Gestures';
@@ -20,6 +21,10 @@ class TabBarModal {
return Selectors.getElementByPlatform(TAB_BAR_ACTION_BUTTON);
}
+ get settingsButton() {
+ return Selectors.getElementByPlatform(TAB_BAR_SETTING_BUTTON);
+ }
+
async tapWalletButton() {
const walletButton = await this.walletButton;
await walletButton.waitForDisplayed();
@@ -40,9 +45,14 @@ class TabBarModal {
async tapActionButton() {
const actionButton = await this.actionButton;
- await actionButton.waitForExist();
+ await actionButton.waitForEnabled();
+ await driver.pause(3000);
await Gestures.longPress(actionButton, 500);
}
+
+ async tapSettingButton() {
+ await Gestures.waitAndTap(this.settingsButton);
+ }
}
export default new TabBarModal();
diff --git a/wdio/screen-objects/NetworksScreen.js b/wdio/screen-objects/NetworksScreen.js
index c578128f839..7f0336c15b0 100644
--- a/wdio/screen-objects/NetworksScreen.js
+++ b/wdio/screen-objects/NetworksScreen.js
@@ -13,6 +13,7 @@ import {
NETWORKS_SYMBOL_INPUT_FIELD,
REMOVE_NETWORK_BUTTON,
} from './testIDs/Screens/NetworksScreen.testids';
+import { ADD_CUSTOM_RPC_NETWORK_BUTTON_ID } from '../../app/constants/test-ids';
class NetworksScreen {
get container() {
@@ -31,6 +32,10 @@ class NetworksScreen {
return Selectors.getElementByPlatform(ADD_NETWORK_BUTTON);
}
+ get addCustomNetworkButton() {
+ return Selectors.getElementByPlatform(ADD_CUSTOM_RPC_NETWORK_BUTTON_ID);
+ }
+
get networkNameInputField() {
return Selectors.getElementByPlatform(INPUT_NETWORK_NAME);
}
@@ -143,23 +148,24 @@ class NetworksScreen {
await expect(this.blockExplorerInputField).toBeDisplayed();
}
- async addButtonNetworkIsDisabled() {
+ async addButtonNetworkIsdisabled() {
await expect(this.addNetworkButton).toHaveAttrContaining(
'clickable',
'false',
);
}
- async tapAddButton() {
- await Gestures.waitAndTap(this.addNetworkButton);
+ async tapCustomAddButton() {
+ await Gestures.waitAndTap(this.addCustomNetworkButton);
}
+
async isDeleteNetworkButtonVisible() {
await expect(this.removeNetworkButton).toBeDisplayed();
}
async tapDeleteNetworkButton() {
- await Gestures.tap(this.removeNetworkButton);
+ await Gestures.waitAndTap(this.removeNetworkButton);
}
async tapSaveNetworkButton() {
@@ -201,12 +207,11 @@ class NetworksScreen {
await Gestures.tapTextByXpath(text);
}
- async isNetworkNameDisplayed(network) {
+ async isNetworknameDisplayed(network) {
expect(await Selectors.getXpathElementByText(network)).toBeDisplayed();
}
async tapBackButtonInNewScreen() {
- await driver.pause(2000);
await Gestures.waitAndTap(this.networkScreenBackButton);
}
diff --git a/wdio/screen-objects/SettingsScreen.js b/wdio/screen-objects/SettingsScreen.js
new file mode 100644
index 00000000000..da90aed4206
--- /dev/null
+++ b/wdio/screen-objects/SettingsScreen.js
@@ -0,0 +1,20 @@
+import Selectors from '../helpers/Selectors';
+import { LOCK_SETTINGS } from './testIDs/Screens/Settings.testIds';
+import Gestures from '../helpers/Gestures';
+
+class SettingsScreen {
+ get lockOption() {
+ return Selectors.getElementByPlatform(LOCK_SETTINGS);
+ }
+
+ async tapLockOption() {
+ const lockOption = await this.lockOption;
+ while (!(await lockOption.isDisplayed())) {
+ await Gestures.swipeUp();
+ }
+
+ await Gestures.waitAndTap(lockOption);
+ }
+}
+
+export default new SettingsScreen();
diff --git a/wdio/screen-objects/TokenOverviewScreen.js b/wdio/screen-objects/TokenOverviewScreen.js
index 83b6a10f4f7..5eee33b3267 100644
--- a/wdio/screen-objects/TokenOverviewScreen.js
+++ b/wdio/screen-objects/TokenOverviewScreen.js
@@ -29,6 +29,8 @@ class TokenOverviewScreen {
}
async tapSendButton() {
+ await Gestures.swipeUp(0.5);
+ await driver.pause(1000);
await Gestures.waitAndTap(this.sendButton);
}
}
diff --git a/wdio/screen-objects/TransactionConfirmScreen.js b/wdio/screen-objects/TransactionConfirmScreen.js
index 4789329cc09..6e9caf7031c 100644
--- a/wdio/screen-objects/TransactionConfirmScreen.js
+++ b/wdio/screen-objects/TransactionConfirmScreen.js
@@ -3,6 +3,8 @@ import {
COMFIRM_TXN_AMOUNT,
CONFIRM_TRANSACTION_BUTTON_ID,
} from './testIDs/Screens/TransactionConfirm.testIds';
+import { ESTIMATED_FEE_TEST_ID } from './testIDs/Screens/TransactionSummaryScreen.testIds';
+import { MAX_PRIORITY_FEE_INPUT_TEST_ID } from './testIDs/Screens/EditGasFeeScreen.testids';
import Gestures from '../helpers/Gestures';
class TransactionConfirmScreen {
@@ -10,10 +12,22 @@ class TransactionConfirmScreen {
return Selectors.getElementByPlatform(COMFIRM_TXN_AMOUNT);
}
+ get estimatedGasFee() {
+ return Selectors.getElementByPlatform(ESTIMATED_FEE_TEST_ID);
+ }
+
get sendButton() {
return Selectors.getElementByPlatform(CONFIRM_TRANSACTION_BUTTON_ID);
}
+ get estimatedGasLink() {
+ return Selectors.getElementByPlatform(ESTIMATED_FEE_TEST_ID);
+ }
+
+ get suggestedGasOptions() {
+ return Selectors.getElementByPlatform(MAX_PRIORITY_FEE_INPUT_TEST_ID);
+ }
+
async isCorrectTokenConfirm(token) {
const confirmAmount = await this.confirmAmount;
await confirmAmount.waitForDisplayed();
@@ -31,14 +45,26 @@ class TransactionConfirmScreen {
await confirmAmount.waitForDisplayed();
}
+ async waitEstimatedGasFeeToDisplay() {
+ const estimatedGasFee = await this.estimatedGasFee;
+ await estimatedGasFee.waitForDisplayed();
+ }
+
async tapSendButton() {
- const sendButton = await this.sendButton;
- await sendButton.waitForDisplayed();
+ await Gestures.waitAndTap(this.sendButton);
+ }
+
+ async tapEstimatedGasLink() {
+ await Gestures.waitAndTap(this.estimatedGasLink);
+ }
+
+ async areSuggestedGasOptionsNotVisible() {
+ const suggestedGasOptions = await this.suggestedGasOptions;
+ await suggestedGasOptions.waitForExist({ reverse: true });
+ }
- while (await sendButton.isExisting()) {
- await Gestures.waitAndTap(this.sendButton);
- await driver.pause(3000);
- }
+ async tapSaveGasButton() {
+ await Gestures.tapTextByXpath('Save');
}
}
diff --git a/wdio/screen-objects/WalletMainScreen.js b/wdio/screen-objects/WalletMainScreen.js
index 908e2cb3711..c3d571d4a02 100644
--- a/wdio/screen-objects/WalletMainScreen.js
+++ b/wdio/screen-objects/WalletMainScreen.js
@@ -7,7 +7,6 @@ import {
} from './testIDs/Components/OnboardingWizard.testIds';
import {
- HAMBURGER_MENU_BUTTON,
IMPORT_NFT_BUTTON_ID,
IMPORT_TOKEN_BUTTON_ID,
MAIN_WALLET_ACCOUNT_ACTIONS,
@@ -20,11 +19,8 @@ import {
SHOW_PRIVATE_KEY,
VIEW_ETHERSCAN,
WALLET_ACCOUNT_ICON,
- WALLET_VIEW_BURGER_ICON_ID,
} from './testIDs/Screens/WalletView.testIds';
-import { DRAWER_VIEW_SETTINGS_TEXT_ID } from './testIDs/Screens/DrawerView.testIds';
-
import { NOTIFICATION_TITLE } from './testIDs/Components/Notification.testIds';
import { TAB_BAR_WALLET_BUTTON } from './testIDs/Components/TabBar.testIds';
import { BACK_BUTTON_SIMPLE_WEBVIEW } from './testIDs/Components/SimpleWebView.testIds';
@@ -42,10 +38,6 @@ class WalletMainScreen {
);
}
- get burgerIcon() {
- return Selectors.getElementByPlatform(WALLET_VIEW_BURGER_ICON_ID);
- }
-
get ImportToken() {
return Selectors.getElementByPlatform(IMPORT_TOKEN_BUTTON_ID);
}
@@ -58,10 +50,6 @@ class WalletMainScreen {
return Selectors.getElementByPlatform(NOTIFICATION_TITLE);
}
- get HamburgerButton() {
- return Selectors.getElementByPlatform(HAMBURGER_MENU_BUTTON);
- }
-
get Identicon() {
return Selectors.getElementByPlatform(WALLET_ACCOUNT_ICON);
}
@@ -74,10 +62,6 @@ class WalletMainScreen {
return Selectors.getElementByPlatform(NAVBAR_NETWORK_BUTTON);
}
- get drawerSettings() {
- return Selectors.getElementByPlatform(DRAWER_VIEW_SETTINGS_TEXT_ID);
- }
-
get mainWalletView() {
return Selectors.getElementByPlatform(MAIN_WALLET_VIEW_VIA_TOKENS_ID);
}
@@ -120,22 +104,18 @@ class WalletMainScreen {
return Selectors.getElementByPlatform(BACK_BUTTON_SIMPLE_WEBVIEW);
}
- async tapSettings() {
- await Gestures.waitAndTap(this.drawerSettings);
+ get zeroBalance() {
+ return Selectors.getXpathElementByText('$0.00');
}
- async tapSendIcon(text) {
- await Gestures.tapTextByXpath(text);
+ get networkModal() {
+ return Selectors.getXpathElementByText('Localhost 8545 now active.');
}
async tapNoThanks() {
await Gestures.waitAndTap(this.noThanks);
}
- async tapBurgerButton() {
- await Gestures.waitAndTap(this.HamburgerButton);
- }
-
async tapImportTokensButton() {
const importToken = await this.ImportToken;
await importToken.waitForDisplayed();
@@ -205,15 +185,15 @@ class WalletMainScreen {
async isSubmittedNotificationDisplayed() {
const element = await this.TokenNotificationTitle;
await element.waitForDisplayed();
- expect(element).toHaveText('Transaction submitted');
+ await expect(element).toHaveText('Transaction submitted');
await element.waitForExist({ reverse: true });
}
async isCompleteNotificationDisplayed() {
const element = await this.TokenNotificationTitle;
await element.waitForDisplayed();
- expect(element).toHaveTextContaining('Transaction');
- expect(element).toHaveTextContaining('Complete!');
+ await expect(element).toHaveTextContaining('Transaction');
+ await expect(element).toHaveTextContaining('Complete!');
await element.waitForExist({ reverse: true });
}
@@ -239,6 +219,11 @@ class WalletMainScreen {
await Gestures.waitAndTap(this.viewEtherscanActionButton);
await Gestures.waitAndTap(this.goBackSimpleWebViewButton);
}
+
+ async waitForNetworkModaltoDisappear() {
+ const element = await this.networkModal;
+ await element.waitForExist({ reverse: true });
+ }
}
export default new WalletMainScreen();
diff --git a/wdio/screen-objects/testIDs/Components/AccountListComponent.testIds.js b/wdio/screen-objects/testIDs/Components/AccountListComponent.testIds.js
index 27b19cae30c..89bde1f038b 100644
--- a/wdio/screen-objects/testIDs/Components/AccountListComponent.testIds.js
+++ b/wdio/screen-objects/testIDs/Components/AccountListComponent.testIds.js
@@ -1,3 +1,5 @@
export const ACCOUNT_LIST_ID = 'account-list';
export const CREATE_ACCOUNT_BUTTON_ID = 'create-account-button';
export const IMPORT_ACCOUNT_BUTTON_ID = 'import-account-button';
+export const ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID = 'account-balance-by-address';
+export const ACCOUNT_LIST_ADD_BUTTON_ID = 'account-list-add-account-button';
diff --git a/wdio/screen-objects/testIDs/Components/AddAccountModal.testIds.js b/wdio/screen-objects/testIDs/Components/AddAccountModal.testIds.js
new file mode 100644
index 00000000000..36b5719c5cf
--- /dev/null
+++ b/wdio/screen-objects/testIDs/Components/AddAccountModal.testIds.js
@@ -0,0 +1,2 @@
+export const ADD_ACCOUNT_NEW_ACCOUNT_BUTTON = 'add-account-new-account';
+export const ADD_ACCOUNT_IMPORT_ACCOUNT_BUTTON = 'add-account-import-account';
diff --git a/wdio/screen-objects/testIDs/Components/TabBar.testIds.js b/wdio/screen-objects/testIDs/Components/TabBar.testIds.js
index db0ce7f2868..6bb67840021 100644
--- a/wdio/screen-objects/testIDs/Components/TabBar.testIds.js
+++ b/wdio/screen-objects/testIDs/Components/TabBar.testIds.js
@@ -3,3 +3,4 @@ export const TAB_BAR_WALLET_BUTTON = 'tab-bar-item-Wallet';
export const TAB_BAR_BROWSER_BUTTON = 'tab-bar-item-Browser';
export const TAB_BAR_ACTION_BUTTON = 'tab-bar-item-Actions';
+export const TAB_BAR_SETTING_BUTTON = 'tab-bar-item-Setting';
diff --git a/wdio/screen-objects/testIDs/Screens/AddressBook.testids.js b/wdio/screen-objects/testIDs/Screens/AddressBook.testids.js
index 41fd0f1cba1..78a2c38569c 100644
--- a/wdio/screen-objects/testIDs/Screens/AddressBook.testids.js
+++ b/wdio/screen-objects/testIDs/Screens/AddressBook.testids.js
@@ -1,2 +1,4 @@
-// eslint-disable-next-line import/prefer-default-export
export const ENTER_ALIAS_INPUT_BOX_ID = 'address-alias-input';
+export const ADDRESS_ALIAS_SAVE_BUTTON_ID = 'address-alias-save-button'
+export const ADDRESS_ALIAS_CANCEL_BUTTON_ID = 'address-alias-cancel'
+export const ADDRESS_ALIAS_TITLE_ID = 'address-alias-title'
diff --git a/wdio/screen-objects/testIDs/Screens/DrawerView.testIds.js b/wdio/screen-objects/testIDs/Screens/DrawerView.testIds.js
index 9121e45474b..ec122b5e656 100644
--- a/wdio/screen-objects/testIDs/Screens/DrawerView.testIds.js
+++ b/wdio/screen-objects/testIDs/Screens/DrawerView.testIds.js
@@ -1,7 +1,5 @@
export const DRAWER_VIEW_LOCK_TEXT_ID = 'drawer-view-lock-text';
-export const DRAWER_VIEW_SETTINGS_TEXT_ID = 'drawer-settings';
-
export const DRAWER_VIEW_BROWSER_TEXT_ID = 'drawer-browser';
export const DRAWER_VIEW_WALLET_TEXT_ID = 'drawer-wallet';
diff --git a/wdio/screen-objects/testIDs/Screens/EditGasFeeScreen.testids.js b/wdio/screen-objects/testIDs/Screens/EditGasFeeScreen.testids.js
index 42747c5bce5..acb408bf045 100644
--- a/wdio/screen-objects/testIDs/Screens/EditGasFeeScreen.testids.js
+++ b/wdio/screen-objects/testIDs/Screens/EditGasFeeScreen.testids.js
@@ -1,2 +1,2 @@
-export const EDIT_PRIOTIRY_SCREEN_TEST_ID = 'edit-priority-screen';
-export const MAX_PRIORITY_FEE_INPUT_TEST_ID = 'max-priority-fee-range-input';
\ No newline at end of file
+export const EDIT_PRIORITY_SCREEN_TEST_ID = 'edit-priority-screen';
+export const MAX_PRIORITY_FEE_INPUT_TEST_ID = 'max-priority-fee-range-input';
diff --git a/wdio/screen-objects/testIDs/Screens/SecurityPrivacy.testIds.js b/wdio/screen-objects/testIDs/Screens/SecurityPrivacy.testIds.js
index 9dc6c0685e7..7709e30a041 100644
--- a/wdio/screen-objects/testIDs/Screens/SecurityPrivacy.testIds.js
+++ b/wdio/screen-objects/testIDs/Screens/SecurityPrivacy.testIds.js
@@ -4,3 +4,6 @@ export const SECURITY_PRIVACY_VIEW_ID = 'security-settings-scrollview';
export const SECURITY_PRIVACY_DELETE_WALLET_BUTTON =
'security-settings-delete-wallet-buttons';
+
+export const SECURITY_PRIVACY_MULTI_ACCOUNT_BALANCES_TOGGLE_ID =
+ 'security-settings-multi-account-balances-switch';
diff --git a/wdio/screen-objects/testIDs/Screens/Settings.testIds.js b/wdio/screen-objects/testIDs/Screens/Settings.testIds.js
new file mode 100644
index 00000000000..517b8490831
--- /dev/null
+++ b/wdio/screen-objects/testIDs/Screens/Settings.testIds.js
@@ -0,0 +1,11 @@
+export const GENERAL_SETTINGS = 'general-settings';
+export const SECURITY_SETTINGS = 'security-settings';
+export const ADVANCED_SETTINGS = 'advanced-settings';
+export const CONTACTS_SETTINGS = 'contacts-settings';
+export const NETWORKS_SETTINGS = 'networks-settings';
+export const ON_RAMP_SETTINGS = 'on-ramp-settings';
+export const EXPERIMENTAL_SETTINGS = 'experimental-settings';
+export const ABOUT_METAMASK_SETTINGS = 'about-metamask-settings';
+export const REQUEST_SETTINGS = 'request-settings';
+export const CONTACT_SETTINGS = 'contact-settings';
+export const LOCK_SETTINGS = 'lock-settings';
diff --git a/wdio/screen-objects/testIDs/Screens/TransactionSummaryScreen.testIds.js b/wdio/screen-objects/testIDs/Screens/TransactionSummaryScreen.testIds.js
index f69afe9fdef..6ad2e07ab53 100644
--- a/wdio/screen-objects/testIDs/Screens/TransactionSummaryScreen.testIds.js
+++ b/wdio/screen-objects/testIDs/Screens/TransactionSummaryScreen.testIds.js
@@ -1 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
export const ESTIMATED_FEE_TEST_ID = 'estimated-fee';
diff --git a/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js b/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js
index 97c71c7a6be..4be4f63eb87 100644
--- a/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js
+++ b/wdio/screen-objects/testIDs/Screens/WalletView.testIds.js
@@ -1,7 +1,3 @@
-export const WALLET_VIEW_BURGER_ICON_ID = 'wallet-view-burger-icon';
-
-export const HAMBURGER_MENU_BUTTON = 'hamburger-menu-button-wallet';
-
export const SEND_BUTTON_ID = 'token-send-button';
export const IMPORT_NFT_BUTTON_ID = 'import-collectible-button';
export const IMPORT_TOKEN_BUTTON_ID = 'import-token-button';
@@ -25,7 +21,6 @@ export const NAVBAR_NETWORK_TEXT = 'open-networks-text';
export const getAssetTestId = (token) => `asset-${token}`;
export const MAIN_WALLET_ACCOUNT_ACTIONS = 'main-wallet-account-actions';
-export const EDIT_ACCOUNT = 'edit-account-action';
export const VIEW_ETHERSCAN = 'view-etherscan-action';
export const SHARE_ADDRESS = 'share-address-action';
export const SHOW_PRIVATE_KEY = 'show-private-key-action';
diff --git a/wdio/step-definitions/add-networks.steps.js b/wdio/step-definitions/add-networks.steps.js
index 42b64de98aa..a037456cfac 100644
--- a/wdio/step-definitions/add-networks.steps.js
+++ b/wdio/step-definitions/add-networks.steps.js
@@ -4,7 +4,8 @@ import NetworksScreen from '../screen-objects/NetworksScreen';
import NetworkApprovalModal from '../screen-objects/Modals/NetworkApprovalModal';
import NetworkEducationModal from '../screen-objects/Modals/NetworkEducationModal';
import NetworkListModal from '../screen-objects/Modals/NetworkListModal';
-import CommonScreen from '../screen-objects/CommonScreen';
+import TabBarModal from '../screen-objects/Modals/TabBarModal';
+import Gestures from '../helpers/Gestures';
When(/^I tap on the Add a Network button/, async () => {
await NetworkListModal.tapAddNetworkButton();
@@ -70,20 +71,6 @@ When(
},
);
-Then(/^I tap on the burger menu/, async () => {
- await WalletMainScreen.tapBurgerButton();
-});
-
-Then(/^I tap on "([^"]*)?" in the menu/, async (option) => {
- switch (option) {
- case 'Settings':
- await WalletMainScreen.tapSettings();
- break;
- default:
- throw new Error('Option not found');
- }
-});
-
Then(/^In settings I tap on "([^"]*)?"/, async (option) => {
await NetworksScreen.tapOptionInSettings(option); // Can be moved later on to more common page object folder
const setTimeout = 2000;
@@ -105,7 +92,7 @@ Then(
/^"([^"]*)?" is not visible in the Popular Networks section/,
async (network) => {
await NetworksScreen.isNetworkNotVisible(network);
- await NetworksScreen.tapBackButton();
+ await NetworksScreen.tapCloseNetworkScreen();
},
);
@@ -163,10 +150,11 @@ Then(/^I specify the following details:/, async () => {
await NetworksScreen.isBlockExplorerUrlVisible();
});
-Then(/^I tap on the Add button/, async () => {
- await driver.hideKeyboard(); // hides keyboard so it can view elements below
- await NetworksScreen.tapAddButton();
- await NetworksScreen.tapAddButton();
+Then(/^I tap on the Add button to add Custom Network/, async () => {
+ await driver.hideKeyboard();
+ await Gestures.swipeUp();
+ await NetworksScreen.tapCustomAddButton();
+ await NetworksScreen.tapCustomAddButton();
});
Then(/^I tap and hold network "([^"]*)?"/, async (network) => {
@@ -234,10 +222,8 @@ Then(/^I navigate back to the main wallet view/, async () => {
});
Then(/^I go back to the main wallet screen/, async () => {
- await driver.pause(2500);
await NetworksScreen.tapBackButtonInNewScreen();
- await driver.pause(2500);
- await NetworksScreen.tapBackButtonInSettingsScreen();
+ await TabBarModal.tapWalletButton();
});
Then(/^I close the networks screen view$/, async () => {
@@ -248,8 +234,7 @@ Given(/^the network screen is displayed$/, async () => {
});
Given(/^Ganache network is selected$/, async () => {
- await WalletMainScreen.tapBurgerButton();
- await WalletMainScreen.tapSettings();
+ await TabBarModal.tapSettingButton();
await NetworksScreen.tapOptionInSettings('Networks');
await NetworksScreen.tapAddNetworkButton();
await driver.hideKeyboard();
@@ -260,9 +245,10 @@ Given(/^Ganache network is selected$/, async () => {
await driver.hideKeyboard();
await NetworksScreen.typeIntoNetworkSymbol('ETH');
await driver.hideKeyboard();
- await NetworksScreen.tapAddButton();
- await NetworksScreen.tapAddButton();
+ await NetworksScreen.tapCustomAddButton();
+ await NetworksScreen.tapCustomAddButton();
await NetworkEducationModal.tapGotItButton();
+ await WalletMainScreen.waitForNetworkModaltoDisappear();
});
Then(
diff --git a/wdio/step-definitions/browser-steps.js b/wdio/step-definitions/browser-steps.js
index f9ca1dc86e0..f503b37b154 100644
--- a/wdio/step-definitions/browser-steps.js
+++ b/wdio/step-definitions/browser-steps.js
@@ -401,7 +401,7 @@ When(/^I connect my active wallet to the test dapp$/, async () => {
});
When(/^I scroll to the ERC20 section$/, async () => {
- await Gestures.swipeUp(0.8);
+ await Gestures.swipeUp(1);
});
When(/^I transfer ERC20 tokens$/, async () => {
@@ -410,6 +410,23 @@ When(/^I transfer ERC20 tokens$/, async () => {
await AccountApprovalModal.waitForDisappear();
});
+When(/^I approve default ERC20 token amount$/, async () => {
+ await ExternalWebsitesScreen.tapDappApproveTokens();
+ await AccountApprovalModal.tapUseDefaultApproveByText();
+ await AccountApprovalModal.tapNextButtonByText();
+ await AccountApprovalModal.tapApproveButtonByText();
+ await AccountApprovalModal.waitForDisappear();
+});
+
+When(/^I approve the custom ERC20 token amount$/, async () => {
+ await ExternalWebsitesScreen.tapDappApproveTokens();
+ await AccountApprovalModal.setTokenAmount('1');
+ await AccountApprovalModal.tapNextButtonByText();
+ await AccountApprovalModal.tapNextButtonByText();
+ await AccountApprovalModal.tapApproveButtonByText();
+ await AccountApprovalModal.waitForDisappear();
+});
+
When(/^I trigger the connect modal$/, async () => {
await ExternalWebsitesScreen.tapDappConnectButton();
});
diff --git a/wdio/step-definitions/common-steps.js b/wdio/step-definitions/common-steps.js
index 556f9bf344c..59d9bbb4967 100644
--- a/wdio/step-definitions/common-steps.js
+++ b/wdio/step-definitions/common-steps.js
@@ -1,14 +1,14 @@
import { Given, Then, When } from '@wdio/cucumber-framework';
+
import Accounts from '../helpers/Accounts';
import WelcomeScreen from '../screen-objects/Onboarding/OnboardingCarousel';
import OnboardingScreen from '../screen-objects/Onboarding/OnboardingScreen';
import MetaMetricsScreen from '../screen-objects/Onboarding/MetaMetricsScreen';
import ImportFromSeedScreen from '../screen-objects/Onboarding/ImportFromSeedScreen';
-
+import TabBarModal from '../screen-objects/Modals/TabBarModal';
import CreateNewWalletScreen from '../screen-objects/Onboarding/CreateNewWalletScreen.js';
import WalletMainScreen from '../screen-objects/WalletMainScreen';
import CommonScreen from '../screen-objects/CommonScreen';
-
import SkipAccountSecurityModal from '../screen-objects/Modals/SkipAccountSecurityModal.js';
import OnboardingWizardModal from '../screen-objects/Modals/OnboardingWizardModal.js';
import LoginScreen from '../screen-objects/LoginScreen';
@@ -133,6 +133,12 @@ Then(/^"([^"]*)?" is displayed on (.*) (.*) view/, async (text) => {
await CommonScreen.isTextDisplayed(text);
});
+Then(/^"([^"]*)?" is displayed/, async (text) => {
+ const timeout = 1000;
+ await driver.pause(timeout);
+ await CommonScreen.isTextDisplayed(text);
+});
+
Then(/^"([^"]*)?" is not displayed/, async (text) => {
const timeout = 1000;
await driver.pause(timeout);
@@ -236,3 +242,7 @@ Given(/^I close the Whats New modal$/, async () => {
await WhatsNewModal.tapCloseButton();
await WhatsNewModal.waitForDisappear();
});
+
+When(/^I tap on the Settings tab option$/, async () => {
+ await TabBarModal.tapSettingButton();
+});
diff --git a/wdio/step-definitions/create-new-wallet-account.steps.js b/wdio/step-definitions/create-new-wallet-account.steps.js
index aa919c1520f..76de1093ff7 100644
--- a/wdio/step-definitions/create-new-wallet-account.steps.js
+++ b/wdio/step-definitions/create-new-wallet-account.steps.js
@@ -3,11 +3,12 @@ import { Then, When } from '@wdio/cucumber-framework';
import AccountListComponent from '../screen-objects/AccountListComponent';
import WalletAccountModal from '../screen-objects/Modals/WalletAccountModal.js';
-import WalletMainScreen from '../screen-objects/WalletMainScreen';
import CommonScreen from '../screen-objects/CommonScreen';
+import AddAccountModal from '../screen-objects/Modals/AddAccountModal';
-Then(/^I tap on Create a new account/, async () => {
- await AccountListComponent.tapCreateAccountButton();
+Then(/^I tap Create a new account/, async () => {
+ await AccountListComponent.tapAddAccountButton();
+ await AddAccountModal.tapNewAccountButton();
});
When(/^A new account is created/, async () => {
@@ -16,7 +17,6 @@ When(/^A new account is created/, async () => {
Then(/^I am on the new account/, async () => {
await CommonScreen.tapOnText('Account 2');
- await WalletMainScreen.tapIdenticon();
await AccountListComponent.isComponentNotDisplayed();
await WalletAccountModal.isAccountNameLabelEqualTo('Account 2');
});
diff --git a/wdio/step-definitions/import-tokens.steps.js b/wdio/step-definitions/import-tokens.steps.js
index b66287bcadc..ff69ce82edc 100644
--- a/wdio/step-definitions/import-tokens.steps.js
+++ b/wdio/step-definitions/import-tokens.steps.js
@@ -1,7 +1,7 @@
import { When, Then } from '@wdio/cucumber-framework';
import AddCustomImportTokensScreen from '../screen-objects/AddCustomImportTokensScreen.js';
import WalletMainScreen from '../screen-objects/WalletMainScreen.js';
-import CommonScreen from "../screen-objects/CommonScreen";
+import CommonScreen from '../screen-objects/CommonScreen';
const setTimeout = 1500; //added to run on physical device
@@ -15,7 +15,7 @@ When(/^I tap (.*) of the token Address field/, async (label) => {
await AddCustomImportTokensScreen.tapTokenSymbolField();
});
-Then( /^The Token Symbol is displayed/, async () => {
+Then(/^The Token Symbol is displayed/, async () => {
await AddCustomImportTokensScreen.tapTokenSymbolField();
await CommonScreen.tapOnText('Token Symbol');
await AddCustomImportTokensScreen.waitForImportButtonEnabled();
diff --git a/wdio/step-definitions/import-wallet-via-private-key.steps.js b/wdio/step-definitions/import-wallet-via-private-key.steps.js
index 5c001e670f1..8de8cc9600d 100644
--- a/wdio/step-definitions/import-wallet-via-private-key.steps.js
+++ b/wdio/step-definitions/import-wallet-via-private-key.steps.js
@@ -1,12 +1,12 @@
-/* eslint-disable no-undef */
import { Then, When } from '@wdio/cucumber-framework';
import AccountListComponent from '../screen-objects/AccountListComponent';
import ImportAccountScreen from '../screen-objects/ImportAccountScreen';
import ImportSuccessScreen from '../screen-objects/ImportSuccessScreen';
+import AddAccountModal from '../screen-objects/Modals/AddAccountModal';
-When(/^I tap on Import an account/, async () => {
- await driver.pause(2000);
- await AccountListComponent.tapImportAccountButton();
+When(/^I tap import account/, async () => {
+ await AccountListComponent.tapAddAccountButton();
+ await AddAccountModal.tapImportAccountButton();
});
Then(/^I am taken to the Import Account screen/, async () => {
diff --git a/wdio/step-definitions/lock-reset-wallet.steps.js b/wdio/step-definitions/lock-reset-wallet.steps.js
new file mode 100644
index 00000000000..690c2009cfc
--- /dev/null
+++ b/wdio/step-definitions/lock-reset-wallet.steps.js
@@ -0,0 +1,6 @@
+import { When } from '@wdio/cucumber-framework';
+import SettingsScreen from '../screen-objects/SettingsScreen';
+
+When(/^In settings I tap on the Lock Option$/, async () => {
+ await SettingsScreen.tapLockOption();
+});
diff --git a/wdio/step-definitions/send-flow.steps.js b/wdio/step-definitions/send-flow.steps.js
index cd4f34556df..1ee8101ab79 100644
--- a/wdio/step-definitions/send-flow.steps.js
+++ b/wdio/step-definitions/send-flow.steps.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line no-unused-vars
import { Given, Then, When } from '@wdio/cucumber-framework';
import SendScreen from '../screen-objects/SendScreen';
import AddressBookModal from '../screen-objects/Modals/AddressBookModal';
@@ -21,6 +20,7 @@ Then(/^the Save button becomes enabled/, async () => {
});
Then(/^I tap the Save button/, async () => {
+ await AddressBookModal.tapTitle();
await AddressBookModal.tapOnSaveButton();
});
@@ -28,7 +28,7 @@ Given(
/^I enter address "([^"]*)?" in the sender's input box/,
async function (address) {
await CommonScreen.checkNoNotification(); // Notification appears a little late and inteferes with clicking function
- switch(address) {
+ switch (address) {
case 'MultisigAddress':
await SendScreen.typeAddressInSendAddressField(this.multisig);
break;
@@ -132,12 +132,14 @@ Then(/^I am taken to the token overview screen/, async () => {
Then(/^I tap back from the Token overview page/, async () => {
await TokenOverviewScreen.tapBackButton();
+ await WalletMainScreen.isMainWalletViewVisible();
});
When(/^I tap button Send on Token screen view$/, async () => {
await TokenOverviewScreen.tapSendButton();
});
When(/^I tap button Send on Confirm Amount view$/, async () => {
+ await TransactionConfirmScreen.waitEstimatedGasFeeToDisplay();
await TransactionConfirmScreen.tapSendButton();
});
@@ -148,3 +150,15 @@ Then(/^the transaction is submitted toast should appeared$/, async () => {
Then(/^Insufficient funds error message should be visible$/, async () => {
await AmountScreen.waitForAmountErrorMessage();
});
+
+When(/^I tap Edit Gas link$/, async () => {
+ await TransactionConfirmScreen.tapEstimatedGasLink();
+});
+
+Then(/^suggested gas options should not be visible$/, async () => {
+ await TransactionConfirmScreen.areSuggestedGasOptionsNotVisible();
+});
+
+When(/^I tap Save Gas Values$/, async () => {
+ await TransactionConfirmScreen.tapSaveGasButton();
+});
diff --git a/wdio/step-definitions/start-exploring.steps.js b/wdio/step-definitions/start-exploring.steps.js
index 890a1e84e67..d8ec1286836 100644
--- a/wdio/step-definitions/start-exploring.steps.js
+++ b/wdio/step-definitions/start-exploring.steps.js
@@ -1,4 +1,4 @@
-import {Given, Then, When} from '@wdio/cucumber-framework';
+import { Given, Then, When } from '@wdio/cucumber-framework';
import OnboardingWizardModal from '../screen-objects/Modals/OnboardingWizardModal.js';
import WalletAccountModal from '../screen-objects/Modals/WalletAccountModal.js';
diff --git a/wdio/step-definitions/wallet-view.steps.js b/wdio/step-definitions/wallet-view.steps.js
index 8edbd59f9fe..90831fc45d9 100644
--- a/wdio/step-definitions/wallet-view.steps.js
+++ b/wdio/step-definitions/wallet-view.steps.js
@@ -11,10 +11,6 @@ Then(/^On the Main Wallet view I tap "([^"]*)?"/, async (text) => {
await CommonScreen.tapOnText(text);
});
-When(/^I tap burger icon/, async () => {
- await WalletMainScreen.tapBurgerButton();
-});
-
When(/^I tap Import Tokens/, async () => {
await WalletMainScreen.tapImportTokensButton();
});
diff --git a/wdio/step-definitions/ganache-steps.js b/wdio/utils/ganache.js
similarity index 64%
rename from wdio/step-definitions/ganache-steps.js
rename to wdio/utils/ganache.js
index 67cb324f1d1..c648bf467f0 100644
--- a/wdio/step-definitions/ganache-steps.js
+++ b/wdio/utils/ganache.js
@@ -1,4 +1,3 @@
-import { Given, Then } from '@wdio/cucumber-framework';
import Accounts from '../helpers/Accounts';
import Ganache from '../../app/util/test/ganache';
import { SMART_CONTRACTS } from '../../app/util/test/smart-contracts';
@@ -7,24 +6,30 @@ import GanacheSeeder from '../../app/util/test/ganache-seeder';
const ganacheServer = new Ganache();
const validAccount = Accounts.getValidAccount();
-Given(/^Ganache server is started$/, async () => {
+export const startGanache = async () => {
await ganacheServer.start({ mnemonic: validAccount.seedPhrase });
-});
+};
-Then(/^Ganache server is stopped$/, async () => {
+export const stopGanache = async () => {
await ganacheServer.quit();
-});
+};
-Given(/^Multisig contract is deployed$/, async function() {
+export const deployMultisig = async () => {
const ganacheSeeder = await new GanacheSeeder(ganacheServer.getProvider());
await ganacheSeeder.deploySmartContract(SMART_CONTRACTS.MULTISIG);
const contractRegistry = ganacheSeeder.getContractRegistry();
- this.multisig = await contractRegistry.getContractAddress(SMART_CONTRACTS.MULTISIG);
-});
+ const multisigAddress = await contractRegistry.getContractAddress(
+ SMART_CONTRACTS.MULTISIG,
+ );
+ return multisigAddress;
+};
-Given(/^ERC20 token contract is deployed$/, async function() {
+export const deployErc20 = async () => {
const ganacheSeeder = await new GanacheSeeder(ganacheServer.getProvider());
await ganacheSeeder.deploySmartContract(SMART_CONTRACTS.HST);
const contractRegistry = ganacheSeeder.getContractRegistry();
- this.erc20 = await contractRegistry.getContractAddress(SMART_CONTRACTS.HST);
-});
\ No newline at end of file
+ const erc20Address = await contractRegistry.getContractAddress(
+ SMART_CONTRACTS.HST,
+ );
+ return erc20Address;
+};
diff --git a/wdio/utils/mocks.js b/wdio/utils/mocks.js
new file mode 100644
index 00000000000..9366ee9aaca
--- /dev/null
+++ b/wdio/utils/mocks.js
@@ -0,0 +1,13 @@
+const nock = require('nock');
+
+export const gasApiDown = () => {
+ const mockServer = nock(new URL('https://gas-api.metaswap.codefi.network'))
+ .persist()
+ .get('/networks/1/suggestedGasFees')
+ .reply(500);
+ return mockServer;
+};
+
+export const cleanAllMocks = () => {
+ nock.cleanAll();
+};
diff --git a/yarn.lock b/yarn.lock
index 554f72c4528..52ec94f63e0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -19367,6 +19367,16 @@ nocache@^2.1.0:
resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
+nock@^13.3.1:
+ version "13.3.1"
+ resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.1.tgz#f22d4d661f7a05ebd9368edae1b5dc0a62d758fc"
+ integrity sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==
+ dependencies:
+ debug "^4.1.0"
+ json-stringify-safe "^5.0.1"
+ lodash "^4.17.21"
+ propagate "^2.0.0"
+
node-abi@^2.18.0, node-abi@^2.21.0, node-abi@^2.7.0:
version "2.30.1"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf"
@@ -21043,6 +21053,11 @@ prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.5.9,
object-assign "^4.1.1"
react-is "^16.8.1"
+propagate@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
+ integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==
+
proper-lockfile@^3.0.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-3.2.0.tgz#89ca420eea1d55d38ca552578851460067bcda66"