diff --git a/.depcheckrc.yml b/.depcheckrc.yml
index c90705901b4..f51f487fd9b 100644
--- a/.depcheckrc.yml
+++ b/.depcheckrc.yml
@@ -2,7 +2,6 @@
ignores:
- '@metamask/oss-attribution-generator'
- 'webpack-cli'
- - '@react-native-community/datetimepicker'
- '@react-native-community/slider'
- 'patch-package'
- '@lavamoat/allow-scripts'
diff --git a/.detoxrc.js b/.detoxrc.js
index d637d3c937c..ad953511261 100644
--- a/.detoxrc.js
+++ b/.detoxrc.js
@@ -26,7 +26,7 @@ module.exports = {
configurations: {
'ios.sim.apiSpecs': {
device: 'ios.simulator',
- app: 'ios.debug',
+ app: 'ios.qa',
testRunner: {
args: {
"$0": "node e2e/api-specs/run-api-spec-tests.js",
@@ -41,10 +41,9 @@ module.exports = {
device: 'ios.simulator',
app: 'ios.release',
},
- // because e2e run on debug mode in bitrise
- 'android.emu.bitrise.debug': {
- device: 'android.bitrise.emulator',
- app: 'android.bitrise.debug',
+ 'ios.sim.qa': {
+ device: 'ios.simulator',
+ app: 'ios.qa',
},
'android.emu.debug': {
@@ -86,32 +85,21 @@ module.exports = {
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MetaMask.app',
build: 'yarn start:ios:e2e',
},
- 'ios.release': {
+ 'ios.qa': {
type: 'ios.app',
binaryPath:
- 'ios/build/Build/Products/Release-iphonesimulator/MetaMask.app',
- build: "METAMASK_BUILD_TYPE='main' METAMASK_ENVIRONMENT='production' yarn build:ios:release:e2e",
- },
- 'android.bitrise.debug': {
- type: 'android.apk',
- binaryPath: 'android/app/build/outputs/apk/prod/debug/app-prod-debug.apk',
- build: 'yarn start:android:e2e',
+ 'ios/build/Build/Products/Release-iphonesimulator/MetaMask-QA.app',
+ build: "METAMASK_BUILD_TYPE='main' METAMASK_ENVIRONMENT='qa' yarn build:ios:qa",
},
'android.debug': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/prod/debug/app-prod-debug.apk',
build: 'yarn start:android:e2e',
},
- 'android.release': {
- type: 'android.apk',
- binaryPath:
- 'android/app/build/outputs/apk/prod/release/app-prod-release.apk',
- build: "METAMASK_BUILD_TYPE='main' METAMASK_ENVIRONMENT='production' yarn build:android:release:e2e",
- },
'android.qa': {
type: 'android.apk',
binaryPath: 'android/app/build/outputs/apk/qa/release/app-qa-release.apk',
- build: "METAMASK_BUILD_TYPE='main' METAMASK_ENVIRONMENT='qa' yarn build:android:qa:e2e",
+ build: "METAMASK_BUILD_TYPE='main' METAMASK_ENVIRONMENT='qa' yarn build:android:qa",
},
},
};
diff --git a/.github/scripts/get-next-semver-version.sh b/.github/scripts/get-next-semver-version.sh
index 8107471978e..552e1fa7061 100755
--- a/.github/scripts/get-next-semver-version.sh
+++ b/.github/scripts/get-next-semver-version.sh
@@ -16,7 +16,7 @@ VERSION_BRANCHES=$(git branch -r | grep -o 'release/[0-9]*\.[0-9]*\.[0-9]*' | gr
VERSION_TAGS=$(git tag | grep -o 'v[0-9]*\.[0-9]*\.[0-9]*' | grep -o '[0-9]*\.[0-9]*\.[0-9]*' | sort --version-sort | tail -n 1)
# Get the version from package.json
-VERSION_PACKAGE=$(node -p "require('./package.json').version")
+VERSION_PACKAGE=$(node -p "require('../../package.json').version")
# Compare versions and keep the highest one
HIGHEST_VERSION=$(printf "%s\n%s\n%s" "$VERSION_BRANCHES" "$VERSION_TAGS" "$VERSION_PACKAGE" | sort --version-sort | tail -n 1)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7978222f4a7..80ffad8fa28 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -229,7 +229,7 @@ android {
release {
manifestPlaceholders.isDebug = false
minifyEnabled enableProguardInReleaseBuilds
- proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+ proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro", "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro", "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules.pro"
}
}
diff --git a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.styles.ts b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.styles.ts
index cda7d7fe248..f755c07b3eb 100644
--- a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.styles.ts
+++ b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.styles.ts
@@ -29,7 +29,6 @@ const styleSheet = StyleSheet.create({
justifyContent: 'flex-start',
},
accountNameLabelText: {
- marginLeft: 4,
paddingHorizontal: 8,
borderWidth: 1,
borderRadius: 8,
diff --git a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx
index 14f619c98e0..88e0f809d05 100644
--- a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx
+++ b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx
@@ -49,15 +49,17 @@ const AccountBase = ({
{accountName}
- {accountTypeLabel && (
+
+ {accountTypeLabel && (
+
{strings(accountTypeLabel)}
- )}
+ )}
diff --git a/app/components/Base/Title/Title.tsx b/app/components/Base/Title/Title.tsx
index ea062e2de3b..71f8eb5ccc4 100644
--- a/app/components/Base/Title/Title.tsx
+++ b/app/components/Base/Title/Title.tsx
@@ -18,12 +18,12 @@ const Title: React.FC = ({
return (
);
diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index a587ef6c3ae..8f424320041 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -131,6 +131,7 @@ import OptionsSheet from '../../UI/SelectOptionSheet/OptionsSheet';
import FoxLoader from '../../../components/UI/FoxLoader';
import { AppStateEventProcessor } from '../../../core/AppStateEventListener';
import MultiRpcModal from '../../../components/Views/MultiRpcModal/MultiRpcModal';
+import { trace, TraceName, TraceOperation } from '../../../util/trace';
const clearStackNavigatorOptions = {
headerShown: false,
@@ -354,7 +355,15 @@ const App = (props) => {
setOnboarded(!!existingUser);
try {
if (existingUser) {
- await Authentication.appTriggeredAuth();
+ await trace(
+ {
+ name: TraceName.BiometricAuthentication,
+ op: TraceOperation.BiometricAuthentication,
+ },
+ async () => {
+ await Authentication.appTriggeredAuth();
+ },
+ );
// we need to reset the navigator here so that the user cannot go back to the login screen
navigator.reset({ routes: [{ name: Routes.ONBOARDING.HOME_NAV }] });
} else {
diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js
index 0d0a523f547..32a35e27754 100644
--- a/app/components/Nav/Main/index.js
+++ b/app/components/Nav/Main/index.js
@@ -492,7 +492,6 @@ const MainFlow = () => (
mode={'modal'}
screenOptions={{
headerShown: false,
- cardStyle: { backgroundColor: importedColors.transparent },
}}
>
diff --git a/app/components/UI/Name/Name.tsx b/app/components/UI/Name/Name.tsx
index 1f338a8b0a7..5e44d36fe8f 100644
--- a/app/components/UI/Name/Name.tsx
+++ b/app/components/UI/Name/Name.tsx
@@ -47,11 +47,21 @@ const UnknownEthereumAddress: React.FC<{ address: string }> = ({ address }) => {
);
};
-const Name: React.FC = ({ type, value }) => {
+const Name: React.FC = ({
+ chainId,
+ preferContractSymbol,
+ type,
+ value,
+}) => {
if (type !== NameType.EthereumAddress) {
throw new Error('Unsupported NameType: ' + type);
}
- const displayName = useDisplayName(type, value);
+ const displayName = useDisplayName(
+ type,
+ value,
+ chainId,
+ preferContractSymbol,
+ );
const { styles } = useStyles(styleSheet, {
displayNameVariant: displayName.variant,
});
diff --git a/app/components/UI/Name/Name.types.ts b/app/components/UI/Name/Name.types.ts
index 007a5077b06..45f7f241b6f 100644
--- a/app/components/UI/Name/Name.types.ts
+++ b/app/components/UI/Name/Name.types.ts
@@ -1,4 +1,5 @@
import { ViewProps } from 'react-native';
+import { Hex } from '@metamask/utils';
/**
* The name types supported by the NameController.
@@ -11,6 +12,8 @@ export enum NameType {
}
export interface NameProperties extends ViewProps {
+ chainId?: Hex;
+ preferContractSymbol?: boolean;
type: NameType;
value: string;
}
diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js
index 8127d67f23c..fd8aa98b1f8 100644
--- a/app/components/UI/Navbar/index.js
+++ b/app/components/UI/Navbar/index.js
@@ -1824,34 +1824,61 @@ export const getSettingsNavigationOptions = (title, themeColors) => {
};
};
-export function getStakingNavbar(title, navigation, themeColors) {
+/**
+ *
+ * @param {String} title - Navbar Title.
+ * @param {NavigationProp} navigation Navigation object returned from useNavigation hook.
+ * @param {ThemeColors} themeColors theme.colors returned from useStyles hook.
+ * @param {{ backgroundColor?: string, hasCancelButton?: boolean, hasBackButton?: boolean }} [options] - Optional options for navbar.
+ * @returns Staking Navbar Component.
+ */
+export function getStakingNavbar(title, navigation, themeColors, options) {
+ const { hasBackButton = true, hasCancelButton = true } = options ?? {};
+
const innerStyles = StyleSheet.create({
+ headerStyle: {
+ backgroundColor:
+ options?.backgroundColor ?? themeColors.background.default,
+ shadowOffset: null,
+ },
+ headerLeft: {
+ marginHorizontal: 16,
+ },
headerButtonText: {
color: themeColors.primary.default,
fontSize: 14,
...fontStyles.normal,
},
- headerStyle: {
- backgroundColor: themeColors.background.default,
- shadowColor: importedColors.transparent,
- elevation: 0,
- },
});
+
+ function navigationPop() {
+ navigation.goBack();
+ }
+
return {
headerTitle: () => (
-
- ),
- headerLeft: () => ,
- headerRight: () => (
- navigation.dangerouslyGetParent()?.pop()}
- style={styles.closeButton}
- >
-
- {strings('navigation.cancel')}
-
-
+ {title}
),
headerStyle: innerStyles.headerStyle,
+ headerLeft: () =>
+ hasBackButton ? (
+
+ ) : null,
+ headerRight: () =>
+ hasCancelButton ? (
+ navigation.dangerouslyGetParent()?.pop()}
+ style={styles.closeButton}
+ >
+
+ {strings('navigation.cancel')}
+
+
+ ) : null,
};
}
diff --git a/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx b/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx
index 1a65102a1ad..c227b206ec5 100644
--- a/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx
+++ b/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx
@@ -5,14 +5,15 @@ import NotificationContent from './Content';
describe('NotificationContent', () => {
const title = 'Welcome to the new Test!';
- const createdAt = '2024-04-26T16:35:03.147606Z';
+ const yesterday = new Date().setDate(new Date().getDate() - 1);
+ const createdAt = new Date(yesterday).toISOString(); // Relative date: one day before current date
const description = {
start:
'We are excited to announce the launch of our brand new website and app!',
end: 'Ethereum',
};
- it('renders correctly', () => {
+ it('render matches snapshot', () => {
const { toJSON } = renderWithProvider(
- 6 months ago
+ Yesterday
@@ -1241,24 +1231,14 @@ exports[`BuildQuote View Crypto Currency Data renders a special error page if cr
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -1952,24 +1932,14 @@ exports[`BuildQuote View Crypto Currency Data renders an error page when there i
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -3582,24 +3552,14 @@ exports[`BuildQuote View Fiat Currency Data renders an error page when there is
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -5212,24 +5172,14 @@ exports[`BuildQuote View Payment Method Data renders an error page when there is
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -6842,24 +6792,14 @@ exports[`BuildQuote View Regions data renders an error page when there is a regi
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -14562,24 +14502,14 @@ exports[`BuildQuote View renders correctly when sdkError is present 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -15244,24 +15174,14 @@ exports[`BuildQuote View renders correctly when sdkError is present 2`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
diff --git a/app/components/UI/Ramp/Views/GetStarted/__snapshots__/GetStarted.test.tsx.snap b/app/components/UI/Ramp/Views/GetStarted/__snapshots__/GetStarted.test.tsx.snap
index e5caeaa4a0f..40c849bc25a 100644
--- a/app/components/UI/Ramp/Views/GetStarted/__snapshots__/GetStarted.test.tsx.snap
+++ b/app/components/UI/Ramp/Views/GetStarted/__snapshots__/GetStarted.test.tsx.snap
@@ -2296,24 +2296,14 @@ exports[`GetStarted renders correctly when sdkError is present 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
diff --git a/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap b/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap
index 92cb998fc5d..da5eb51bb0e 100644
--- a/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap
+++ b/app/components/UI/Ramp/Views/NetworkSwitcher/__snapshots__/NetworkSwitcher.test.tsx.snap
@@ -8400,24 +8400,14 @@ exports[`NetworkSwitcher View renders correctly with errors 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -9059,24 +9049,14 @@ exports[`NetworkSwitcher View renders correctly with errors 2`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -9718,24 +9698,14 @@ exports[`NetworkSwitcher View renders correctly with no data 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
diff --git a/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap b/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap
index b6ab62534d5..06315a15e53 100644
--- a/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap
+++ b/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap
@@ -8415,24 +8415,14 @@ exports[`OrderDetails renders an error screen if a CREATED order cannot be polle
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
diff --git a/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap b/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap
index 5c0641f5780..9f335593db4 100644
--- a/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap
+++ b/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap
@@ -4697,24 +4697,14 @@ exports[`PaymentMethods View renders correctly with empty data 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -5381,24 +5371,14 @@ exports[`PaymentMethods View renders correctly with empty data for sell 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -6065,24 +6045,14 @@ exports[`PaymentMethods View renders correctly with error 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -9509,24 +9479,14 @@ exports[`PaymentMethods View renders correctly with sdkError 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
diff --git a/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap b/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap
index b66ad3b7fd6..7a16475b13c 100644
--- a/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap
+++ b/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap
@@ -1095,24 +1095,14 @@ exports[`Quotes renders animation on first fetching 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -6515,24 +6505,14 @@ exports[`Quotes renders correctly after animation without quotes 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -7283,24 +7263,14 @@ exports[`Quotes renders correctly when fetching quotes errors 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -8051,24 +8021,14 @@ exports[`Quotes renders correctly with sdkError 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -8819,24 +8779,14 @@ exports[`Quotes renders quotes expired screen 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
diff --git a/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap b/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap
index d513139d165..4d872304a48 100644
--- a/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap
+++ b/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap
@@ -1934,24 +1934,14 @@ exports[`Regions View renders correctly with error 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
@@ -3204,24 +3194,14 @@ exports[`Regions View renders correctly with sdkError 1`] = `
accessibilityRole="text"
style={
{
- "0": {
- "color": "#141618",
- "fontFamily": "EuclidCircularB-Bold",
- "fontSize": 18,
- "fontWeight": "600",
- "marginVertical": 3,
- },
- "1": {
- "textAlign": "center",
- },
- "2": undefined,
- "3": undefined,
"color": "#141618",
- "fontFamily": "EuclidCircularB-Regular",
- "fontSize": 14,
- "fontWeight": "400",
+ "fontFamily": "EuclidCircularB-Bold",
+ "fontSize": 18,
+ "fontWeight": "600",
"letterSpacing": 0,
"lineHeight": 22,
+ "marginVertical": 3,
+ "textAlign": "center",
}
}
>
diff --git a/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx b/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx
index 96edaf2b2a3..534c0d58c50 100644
--- a/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx
+++ b/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx
@@ -63,9 +63,10 @@ const AssetPill: React.FC = ({ asset }) => {
) : (
)}
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.styles.ts b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.styles.ts
new file mode 100644
index 00000000000..d351fc7302a
--- /dev/null
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.styles.ts
@@ -0,0 +1,23 @@
+import type { Theme } from '../../../../../util/theme/models';
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = (params: { theme: Theme }) => {
+ const { theme } = params;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ mainContainer: {
+ flex: 1,
+ paddingTop: 8,
+ paddingHorizontal: 16,
+ backgroundColor: colors.background.alternative,
+ justifyContent: 'space-between',
+ },
+ cardsContainer: {
+ paddingTop: 16,
+ gap: 8,
+ },
+ });
+};
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx
new file mode 100644
index 00000000000..109fe3e7fac
--- /dev/null
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import renderWithProvider from '../../../../../util/test/renderWithProvider';
+import StakeConfirmationView from './StakeConfirmationView';
+import { Image } from 'react-native';
+import { createMockAccountsControllerState } from '../../../../../util/test/accountsControllerTestUtils';
+import { backgroundState } from '../../../../../util/test/initial-root-state';
+import configureMockStore from 'redux-mock-store';
+import { Provider } from 'react-redux';
+import { StakeConfirmationViewProps } from './StakeConfirmationView.types';
+
+jest.mock('../../../../hooks/useIpfsGateway', () => jest.fn());
+
+Image.getSize = jest.fn((_uri, success) => {
+ success(100, 100); // Mock successful response for ETH native Icon Image
+});
+
+const MOCK_ADDRESS_1 = '0x0';
+const MOCK_ADDRESS_2 = '0x1';
+
+const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([
+ MOCK_ADDRESS_1,
+ MOCK_ADDRESS_2,
+]);
+
+const mockStore = configureMockStore();
+
+const mockInitialState = {
+ settings: {},
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
+ },
+ },
+};
+const store = mockStore(mockInitialState);
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest
+ .fn()
+ .mockImplementation((callback) => callback(mockInitialState)),
+}));
+
+jest.mock('@react-navigation/native', () => {
+ const actualNav = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualNav,
+ useNavigation: () => ({
+ navigate: jest.fn(),
+ setOptions: jest.fn(),
+ }),
+ };
+});
+
+describe('StakeConfirmationView', () => {
+ it('render matches snapshot', () => {
+ const props: StakeConfirmationViewProps = {
+ route: {
+ key: '1',
+ params: { amountWei: '3210000000000000', amountFiat: '7.46' },
+ name: 'params',
+ },
+ };
+
+ const { toJSON } = renderWithProvider(
+
+
+ ,
+ );
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx
new file mode 100644
index 00000000000..2f1a4890286
--- /dev/null
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx
@@ -0,0 +1,60 @@
+import React, { useEffect } from 'react';
+import { View } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { useStyles } from '../../../../hooks/useStyles';
+import { getStakingNavbar } from '../../../Navbar';
+import styleSheet from './StakeConfirmationView.styles';
+import TokenValueStack from '../../components/StakingConfirmation/TokenValueStack/TokenValueStack';
+import AccountHeaderCard from '../../components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard';
+import RewardsCard from '../../components/StakingConfirmation/RewardsCard/RewardsCard';
+import ConfirmationFooter from '../../components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter';
+import { StakeConfirmationViewProps } from './StakeConfirmationView.types';
+import { MOCK_GET_VAULT_RESPONSE } from '../../components/StakingBalance/mockData';
+import { strings } from '../../../../../../locales/i18n';
+
+const MOCK_REWARD_DATA = {
+ REWARDS: {
+ ETH: '0.13 ETH',
+ FIAT: '$334.93',
+ },
+};
+
+const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking';
+
+const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => {
+ const navigation = useNavigation();
+
+ const { styles, theme } = useStyles(styleSheet, {});
+
+ useEffect(() => {
+ navigation.setOptions(
+ getStakingNavbar(strings('stake.stake'), navigation, theme.colors, {
+ backgroundColor: theme.colors.background.alternative,
+ hasCancelButton: false,
+ }),
+ );
+ }, [navigation, theme.colors]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default StakeConfirmationView;
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts
new file mode 100644
index 00000000000..8c723135f4f
--- /dev/null
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts
@@ -0,0 +1,10 @@
+import { RouteProp } from '@react-navigation/native';
+
+interface StakeConfirmationViewRouteParams {
+ amountWei: string;
+ amountFiat: string;
+}
+
+export interface StakeConfirmationViewProps {
+ route: RouteProp<{ params: StakeConfirmationViewRouteParams }, 'params'>;
+}
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap
new file mode 100644
index 00000000000..9d14c100f63
--- /dev/null
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap
@@ -0,0 +1,1424 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`StakeConfirmationView render matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.00321
+
+ ETH
+
+
+ $7.46
+
+
+
+
+
+
+
+
+
+
+
+ Staking from
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account 1
+
+
+
+
+
+
+
+
+
+
+
+
+ Interacting with
+
+
+
+
+
+
+
+
+
+
+
+
+ MM Pooled Staking
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Network
+
+
+
+
+
+
+
+
+
+
+
+
+ Ethereum Main Network
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reward rate
+
+
+
+
+
+
+
+
+
+
+
+ 2.8%
+
+
+
+
+
+
+
+
+
+
+ Estimated annual rewards
+
+
+
+
+
+
+
+
+
+ $334.93
+
+
+ 0.13 ETH
+
+
+
+
+
+
+
+
+
+
+
+ Reward frequency
+
+
+
+
+
+
+
+
+
+
+
+ 12 hours
+
+
+
+
+
+
+
+
+
+
+
+
+ Terms of service
+
+
+
+
+ Risk disclosure
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ Confirm
+
+
+
+
+
+`;
diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
index 1ca6560d7f4..0431e67a77f 100644
--- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
+++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
@@ -50,8 +50,14 @@ const StakeInputView = () => {
};
const handleStakePress = useCallback(() => {
- // TODO: Display the Review bottom sheet: STAKE-824
- }, []);
+ navigation.navigate('StakeScreens', {
+ screen: Routes.STAKING.STAKE_CONFIRMATION,
+ params: {
+ amountWei: amountWei.toString(),
+ amountFiat: fiatAmount,
+ },
+ });
+ }, [amountWei, fiatAmount, navigation]);
const balanceText = strings('stake.balance');
@@ -66,7 +72,11 @@ const StakeInputView = () => {
: `${balanceFiatNumber?.toString()} ${currentCurrency.toUpperCase()}`;
useEffect(() => {
- navigation.setOptions(getStakingNavbar(title, navigation, theme.colors));
+ navigation.setOptions(
+ getStakingNavbar(title, navigation, theme.colors, {
+ hasBackButton: false,
+ }),
+ );
}, [navigation, theme.colors, title]);
useEffect(() => {
diff --git a/app/components/UI/Stake/Views/StakeInputView/__snapshots__/StakeInputView.test.tsx.snap b/app/components/UI/Stake/Views/StakeInputView/__snapshots__/StakeInputView.test.tsx.snap
index 5085ab0c15e..4a35aa2b83e 100644
--- a/app/components/UI/Stake/Views/StakeInputView/__snapshots__/StakeInputView.test.tsx.snap
+++ b/app/components/UI/Stake/Views/StakeInputView/__snapshots__/StakeInputView.test.tsx.snap
@@ -56,13 +56,9 @@ exports[`StakeInputView render matches snapshot 1`] = `
{
"backgroundColor": "#ffffff",
"borderBottomColor": "rgb(216, 216, 216)",
- "elevation": 0,
"flex": 1,
- "shadowColor": "transparent",
- "shadowOffset": {
- "height": 0.5,
- "width": 0,
- },
+ "shadowColor": "rgb(216, 216, 216)",
+ "shadowOffset": null,
"shadowOpacity": 0.85,
"shadowRadius": 0,
}
@@ -106,96 +102,26 @@ exports[`StakeInputView render matches snapshot 1`] = `
pointerEvents="box-none"
style={
{
- "alignItems": "flex-start",
- "bottom": 0,
- "justifyContent": "center",
- "left": 0,
- "opacity": 1,
- "position": "absolute",
- "top": 0,
- }
- }
- >
-
-
-
-
-
- Stake ETH
-
-
-
-
- Ethereum Main Network
-
-
-
+ Stake ETH
+
{
: strings('stake.review');
useEffect(() => {
- navigation.setOptions(getStakingNavbar(title, navigation, theme.colors));
+ navigation.setOptions(
+ getStakingNavbar(title, navigation, theme.colors, {
+ hasBackButton: false,
+ }),
+ );
}, [navigation, theme.colors, title]);
const handleUnstakePress = useCallback(() => {
diff --git a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap
index 15e289f23e7..5e7927b0b5c 100644
--- a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap
+++ b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap
@@ -56,13 +56,9 @@ exports[`UnstakeInputView render matches snapshot 1`] = `
{
"backgroundColor": "#ffffff",
"borderBottomColor": "rgb(216, 216, 216)",
- "elevation": 0,
"flex": 1,
- "shadowColor": "transparent",
- "shadowOffset": {
- "height": 0.5,
- "width": 0,
- },
+ "shadowColor": "rgb(216, 216, 216)",
+ "shadowOffset": null,
"shadowOpacity": 0.85,
"shadowRadius": 0,
}
@@ -106,96 +102,26 @@ exports[`UnstakeInputView render matches snapshot 1`] = `
pointerEvents="box-none"
style={
{
- "alignItems": "flex-start",
- "bottom": 0,
- "justifyContent": "center",
- "left": 0,
- "opacity": 1,
- "position": "absolute",
- "top": 0,
- }
- }
- >
-
-
-
-
-
- Unstake ETH
-
-
-
-
- Ethereum Main Network
-
-
-
+ Unstake ETH
+
jest.fn());
+
+Image.getSize = jest.fn((_uri, success) => {
+ success(100, 100); // Mock successful response for ETH native Icon Image
+});
const mockNavigate = jest.fn();
@@ -39,15 +29,17 @@ afterEach(() => {
});
describe('StakingBalance', () => {
+ beforeEach(() => jest.resetAllMocks());
+
it('render matches snapshot', () => {
- render(StakingBalance);
- expect(screen.toJSON()).toMatchSnapshot();
+ const { toJSON } = renderWithProvider();
+ expect(toJSON()).toMatchSnapshot();
});
it('redirects to StakeInputView on stake button click', () => {
- render(StakingBalance);
+ const { getByText } = renderWithProvider();
- fireEvent.press(screen.getByText(strings('stake.stake_more')));
+ fireEvent.press(getByText(strings('stake.stake_more')));
expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith('StakeScreens', {
@@ -56,9 +48,9 @@ describe('StakingBalance', () => {
});
it('redirects to UnstakeInputView on unstake button click', () => {
- render(StakingBalance);
+ const { getByText } = renderWithProvider();
- fireEvent.press(screen.getByText(strings('stake.unstake')));
+ fireEvent.press(getByText(strings('stake.unstake')));
expect(mockNavigate).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith('StakeScreens', {
diff --git a/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx b/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx
index 1ab816b649d..13b3d2c8629 100644
--- a/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx
+++ b/app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx
@@ -36,10 +36,10 @@ const StakingCta = ({ estimatedRewardRate, style }: StakingCtaProps) => {
{strings('stake.stake_your_eth_cta.base')}
- {estimatedRewardRate}
-
- {strings('stake.stake_your_eth_cta.annually')}
+
+ {estimatedRewardRate}
+ {strings('stake.stake_your_eth_cta.annually')}
+
-
+ $13,292.20
+
+
-
+
+
+
+
+
+
-
+
+
+ Unstaking 0.0010 ETH in progress. Come back in 11 days to claim it.
+
+
+
+
+
+
+
+
+
-
+
+
-
+
+
+
+
+
+ Stake ETH and earn
+
+
+
+ Stake your ETH with MetaMask Pool and earn
+
+
+ 2.9%
+
+
+ annually.
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Staked Ethereum
-
-
-
- $13,292.20
-
-
- 4.9999 ETH
-
-
-
-
-
-
-
-
-
-
- Unstaking 0.0010 ETH in progress. Come back in 11 days to claim it.
-
-
-
-
-
-
-
-
-
- You can claim 0.00214 ETH. Once claimed, you'll get ETH back in your wallet.
-
-
-
- Claim
- ETH
-
-
-
-
-
-
- Stake ETH and earn
-
-
-
- Stake your ETH with MetaMask Pool and earn
-
-
- 2.9%
-
-
- annually.
-
-
-
- Learn more.
-
-
-
-
-
-
-
- Unstake
-
-
-
-
- Stake more
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ }
+ >
+ Learn more.
+
+
+
+
+
+
+
+ Unstake
+
+
+
+
+ Stake more
+
+
+
+
`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.styles.ts
new file mode 100644
index 00000000000..3688107b7e1
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.styles.ts
@@ -0,0 +1,40 @@
+import type { Theme } from '../../../../../../util/theme/models';
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = (params: { theme: Theme }) => {
+ const { theme } = params;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ cardGroupTop: {
+ borderWidth: 0,
+ gap: 16,
+ borderRadius: 8,
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ cardGroupBottom: {
+ borderLeftWidth: 0,
+ borderRightWidth: 0,
+ borderBottomWidth: 0,
+ borderTopLeftRadius: 0,
+ borderTopRightRadius: 0,
+ borderBottomLeftRadius: 8,
+ borderBottomRightRadius: 8,
+ borderColor: colors.border.muted,
+ },
+ networkKeyValueRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ tagMinimalPadding: {
+ paddingLeft: 0,
+ paddingRight: 8,
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
+ });
+};
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.test.tsx
new file mode 100644
index 00000000000..c8aa996b8f9
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.test.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import AccountHeaderCard from './AccountHeaderCard';
+import { strings } from '../../../../../../../locales/i18n';
+import { createMockAccountsControllerState } from '../../../../../../util/test/accountsControllerTestUtils';
+import configureMockStore from 'redux-mock-store';
+import { backgroundState } from '../../../../../../util/test/initial-root-state';
+import { Provider } from 'react-redux';
+import { AccountHeaderCardProps } from './AccountHeaderCard.types';
+
+const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking';
+
+const MOCK_ADDRESS_1 = '0x0';
+const MOCK_ADDRESS_2 = '0x1';
+
+const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([
+ MOCK_ADDRESS_1,
+ MOCK_ADDRESS_2,
+]);
+
+const mockStore = configureMockStore();
+
+const mockInitialState = {
+ settings: {},
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
+ },
+ },
+};
+const store = mockStore(mockInitialState);
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest
+ .fn()
+ .mockImplementation((callback) => callback(mockInitialState)),
+}));
+
+const mockNavigate = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ }),
+ };
+});
+
+describe('AccountHeaderCard', () => {
+ it('render matches snapshot', () => {
+ const props: AccountHeaderCardProps = {
+ contractName: MOCK_STAKING_CONTRACT_NAME,
+ };
+
+ const { getByText, toJSON } = renderWithProvider(
+
+ ,
+ ,
+ );
+
+ expect(getByText(strings('stake.staking_from'))).toBeDefined();
+ expect(getByText(strings('stake.interacting_with'))).toBeDefined();
+ expect(getByText(strings('asset_details.network'))).toBeDefined();
+ expect(getByText(props.contractName)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.tsx b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.tsx
new file mode 100644
index 00000000000..9d4e2cb19ee
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { View } from 'react-native';
+import { useSelector } from 'react-redux';
+import { strings } from '../../../../../../../locales/i18n';
+import KeyValueRow from '../../../../../../component-library/components-temp/KeyValueRow';
+import Avatar, {
+ AvatarVariant,
+ AvatarSize,
+} from '../../../../../../component-library/components/Avatars/Avatar';
+import Text from '../../../../../../component-library/components/Texts/Text';
+import { selectSelectedInternalAccount } from '../../../../../../selectors/accountsController';
+import { useStyles } from '../../../../../hooks/useStyles';
+import Card from '../../../../../../component-library/components/Cards/Card';
+import styleSheet from './AccountHeaderCard.styles';
+import images from '../../../../../../images/image-icons';
+import AccountTag from '../AccountTag/AccountTag';
+import { selectNetworkName } from '../../../../../../selectors/networkInfos';
+import { AccountHeaderCardProps } from './AccountHeaderCard.types';
+import ContractTag from '../ContractTag/ContractTag';
+
+const AccountHeaderCard = ({ contractName }: AccountHeaderCardProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const account = useSelector(selectSelectedInternalAccount);
+
+ const networkName = useSelector(selectNetworkName);
+
+ return (
+
+
+ {account && (
+
+ ),
+ }}
+ />
+ )}
+ ,
+ }}
+ />
+
+
+
+
+ {networkName}
+
+ ),
+ }}
+ />
+
+
+ );
+};
+
+export default AccountHeaderCard;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.types.ts b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.types.ts
new file mode 100644
index 00000000000..3775cc7f14a
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/AccountHeaderCard.types.ts
@@ -0,0 +1,3 @@
+export interface AccountHeaderCardProps {
+ contractName: string;
+}
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/__snapshots__/AccountHeaderCard.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/__snapshots__/AccountHeaderCard.test.tsx.snap
new file mode 100644
index 00000000000..261f776ab14
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountHeaderCard/__snapshots__/AccountHeaderCard.test.tsx.snap
@@ -0,0 +1,623 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AccountHeaderCard render matches snapshot 1`] = `
+[
+
+
+
+
+
+
+
+ Staking from
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account 1
+
+
+
+
+
+
+
+
+
+
+
+
+ Interacting with
+
+
+
+
+
+
+
+
+
+
+
+
+ MM Pooled Staking
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Network
+
+
+
+
+
+
+
+
+
+
+
+
+ Ethereum Main Network
+
+
+
+
+
+
+
+ ,
+ ",",
+]
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.test.tsx
new file mode 100644
index 00000000000..901690c1847
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.test.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import AccountTag from './AccountTag';
+import { AccountTagProps } from './AccountTag.types';
+
+describe('AccountTag', () => {
+ it('render matches snapshot when name prop is defined', () => {
+ const props: AccountTagProps = {
+ accountAddress: '0x1',
+ accountName: 'Sample Contract',
+ };
+
+ const { getByText, toJSON } = renderWithProvider();
+
+ expect(getByText(props.accountName as string)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it("render matches snapshot when name prop isn't defined", () => {
+ const props: AccountTagProps = {
+ accountAddress: '0x1',
+ };
+
+ const { getByText, toJSON } = renderWithProvider();
+
+ expect(getByText(props.accountAddress)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.tsx b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.tsx
new file mode 100644
index 00000000000..70a8986edd0
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import TagBase, {
+ TagShape,
+ TagSeverity,
+} from '../../../../../../component-library/base-components/TagBase';
+import Avatar, {
+ AvatarVariant,
+ AvatarSize,
+ AvatarAccountType,
+} from '../../../../../../component-library/components/Avatars/Avatar';
+import { AccountTagProps } from './AccountTag.types';
+
+const AccountTag = ({
+ accountAddress,
+ accountName,
+ useBlockieIcon = false,
+}: AccountTagProps) => (
+
+ }
+ shape={TagShape.Pill}
+ severity={TagSeverity.Info}
+ >
+ {accountName ?? accountAddress}
+
+);
+
+export default AccountTag;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.types.ts b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.types.ts
new file mode 100644
index 00000000000..24866d2b492
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/AccountTag.types.ts
@@ -0,0 +1,5 @@
+export interface AccountTagProps {
+ accountAddress: string;
+ accountName?: string;
+ useBlockieIcon?: boolean;
+}
diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountTag/__snapshots__/AccountTag.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/__snapshots__/AccountTag.test.tsx.snap
new file mode 100644
index 00000000000..ca2dc96db44
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/AccountTag/__snapshots__/AccountTag.test.tsx.snap
@@ -0,0 +1,395 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AccountTag render matches snapshot when name prop is defined 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sample Contract
+
+
+
+`;
+
+exports[`AccountTag render matches snapshot when name prop isn't defined 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0x1
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.styles.ts
new file mode 100644
index 00000000000..de5fe494916
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.styles.ts
@@ -0,0 +1,114 @@
+import type { Theme } from '../../../../../../util/theme/models';
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = (params: { theme: Theme }) => {
+ const { theme } = params;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ mainContainer: {
+ paddingTop: 8,
+ paddingHorizontal: 16,
+ backgroundColor: colors.background.alternative,
+ height: '100%',
+ justifyContent: 'space-between',
+ },
+ // Card styles
+ cardsContainer: {
+ paddingTop: 8,
+ gap: 8,
+ },
+ card: {
+ borderWidth: 0,
+ gap: 16,
+ borderRadius: 8,
+ },
+ estGasFeeCard: {
+ borderWidth: 0,
+ gap: 16,
+ borderRadius: 8,
+ justifyContent: 'center',
+ },
+ cardGroupTop: {
+ borderWidth: 0,
+ gap: 16,
+ borderRadius: 8,
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ cardGroupBottom: {
+ borderLeftWidth: 0,
+ borderRightWidth: 0,
+ borderBottomWidth: 0,
+ borderTopLeftRadius: 0,
+ borderTopRightRadius: 0,
+ borderBottomLeftRadius: 8,
+ borderBottomRightRadius: 8,
+ borderColor: colors.border.muted,
+ },
+ // Network
+ networkKeyValueRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ // Estimated Gas Fee
+ estGasFeeValue: {
+ flexDirection: 'row',
+ paddingTop: 1,
+ },
+ foxIcon: {
+ paddingRight: 8,
+ },
+ fiatText: {
+ paddingRight: 4,
+ },
+ ethText: {
+ borderBottomWidth: 1,
+ borderBottomColor: theme.colors.primary.default,
+ },
+ estimatedGasTooltipContent: {
+ gap: 16,
+ },
+ gasLearnMoreLink: {
+ alignSelf: 'flex-start',
+ },
+ // Est. Annual Reward
+ estAnnualRewardValue: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ // Tags
+ tagMinimalPadding: {
+ paddingLeft: 0,
+ paddingRight: 8,
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
+ // Terms of Service / Risk Disclosure Button Group
+ termsOfServiceButtonGroup: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ legalLink: {
+ padding: 16,
+ },
+ // Footer Button Group
+ footerButtonGroup: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ gap: 16,
+ paddingTop: 24,
+ },
+ footerButton: {
+ flexGrow: 1,
+ flexShrink: 0,
+ flexBasis: 0,
+ },
+ footerContainer: {
+ paddingBottom: 40,
+ },
+ });
+};
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.test.tsx
new file mode 100644
index 00000000000..36db2a000b1
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.test.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import ConfirmationFooter from './ConfirmationFooter';
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: jest.fn(),
+ };
+});
+
+describe('ConfirmationFooter', () => {
+ it('render matches snapshot', () => {
+ const { toJSON } = renderWithProvider();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.tsx
new file mode 100644
index 00000000000..6505cef13f7
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { useStyles } from '../../../../../hooks/useStyles';
+import styleSheet from './ConfirmationFooter.styles';
+import { View } from 'react-native';
+import FooterLegalLinks from './LegalLinks/LegalLinks';
+import FooterButtonGroup from './FooterButtonGroup/FooterButtonGroup';
+
+const ConfirmationFooter = () => {
+ const { styles } = useStyles(styleSheet, {});
+
+ return (
+
+
+
+
+ );
+};
+
+export default ConfirmationFooter;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.styles.ts
new file mode 100644
index 00000000000..c50133567a7
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.styles.ts
@@ -0,0 +1,18 @@
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = () =>
+ StyleSheet.create({
+ footerContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ gap: 16,
+ paddingTop: 24,
+ },
+ button: {
+ flexGrow: 1,
+ flexShrink: 0,
+ flexBasis: 0,
+ },
+ });
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx
new file mode 100644
index 00000000000..9f756063fa6
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../../util/test/renderWithProvider';
+import { strings } from '../../../../../../../../locales/i18n';
+import FooterButtonGroup from './FooterButtonGroup';
+import { fireEvent } from '@testing-library/react-native';
+
+const mockCanGoBack = jest.fn();
+const mockGoBack = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ canGoBack: mockCanGoBack,
+ goBack: mockGoBack,
+ }),
+ };
+});
+
+describe('FooterButtonGroup', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('render matches snapshot', () => {
+ const { getByText, toJSON } = renderWithProvider();
+
+ expect(getByText(strings('stake.cancel'))).toBeDefined();
+ expect(getByText(strings('stake.confirm'))).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('navigates to previous page when cancel button is pressed', () => {
+ mockCanGoBack.mockImplementationOnce(() => true);
+
+ const { getByText, toJSON } = renderWithProvider();
+
+ fireEvent.press(getByText(strings('stake.cancel')));
+
+ expect(mockCanGoBack).toHaveBeenCalledTimes(1);
+ expect(mockGoBack).toHaveBeenCalledTimes(1);
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it.todo('confirms stake when confirm button is pressed');
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx
new file mode 100644
index 00000000000..bd781bfc9a8
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useNavigation } from '@react-navigation/native';
+import { View } from 'react-native';
+import { strings } from '../../../../../../../../locales/i18n';
+import Button, {
+ ButtonVariants,
+ ButtonWidthTypes,
+ ButtonSize,
+} from '../../../../../../../component-library/components/Buttons/Button';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../../../../hooks/useStyles';
+import styleSheet from './FooterButtonGroup.styles';
+
+const FooterButtonGroup = () => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const navigation = useNavigation();
+
+ const handleGoBack = () => {
+ if (navigation.canGoBack()) {
+ navigation.goBack();
+ }
+ };
+
+ return (
+
+
+ );
+};
+
+export default FooterButtonGroup;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/__snapshots__/FooterButtonGroup.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/__snapshots__/FooterButtonGroup.test.tsx.snap
new file mode 100644
index 00000000000..df88108ac02
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/__snapshots__/FooterButtonGroup.test.tsx.snap
@@ -0,0 +1,189 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FooterButtonGroup navigates to previous page when cancel button is pressed 1`] = `
+
+
+
+ Cancel
+
+
+
+
+ Confirm
+
+
+
+`;
+
+exports[`FooterButtonGroup render matches snapshot 1`] = `
+
+
+
+ Cancel
+
+
+
+
+ Confirm
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.styles.ts
new file mode 100644
index 00000000000..a5ac5b5ff78
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.styles.ts
@@ -0,0 +1,14 @@
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = () =>
+ StyleSheet.create({
+ termsOfServiceButtonGroup: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+ legalLink: {
+ padding: 16,
+ },
+ });
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.test.tsx
new file mode 100644
index 00000000000..5338556c843
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.test.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../../util/test/renderWithProvider';
+import FooterLegalLinks from './LegalLinks';
+import { strings } from '../../../../../../../../locales/i18n';
+import { fireEvent } from '@testing-library/react-native';
+import AppConstants from '../../../../../../../core/AppConstants';
+
+const mockNavigate = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ }),
+ };
+});
+
+describe('FooterLegalLinks', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('render matches snapshot', () => {
+ const { getByText, toJSON } = renderWithProvider();
+
+ expect(getByText(strings('stake.terms_of_service'))).toBeDefined();
+ expect(getByText(strings('stake.risk_disclosure'))).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('navigates to terms of use web page', () => {
+ const { getByText, toJSON } = renderWithProvider();
+
+ fireEvent.press(getByText(strings('stake.terms_of_service')));
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith('Webview', {
+ params: { url: AppConstants.URLS.TERMS_AND_CONDITIONS },
+ screen: 'SimpleWebview',
+ });
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('navigates to risk disclosure web page', () => {
+ const { getByText, toJSON } = renderWithProvider();
+
+ fireEvent.press(getByText(strings('stake.risk_disclosure')));
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith('Webview', {
+ params: { url: AppConstants.URLS.STAKING_RISK_DISCLOSURE },
+ screen: 'SimpleWebview',
+ });
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.tsx
new file mode 100644
index 00000000000..4b74efd0bcc
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/LegalLinks.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { useNavigation } from '@react-navigation/native';
+import { View, TouchableOpacity } from 'react-native';
+import { strings } from '../../../../../../../../locales/i18n';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../../../component-library/components/Texts/Text';
+import AppConstants from '../../../../../../../core/AppConstants';
+import { useStyles } from '../../../../../../hooks/useStyles';
+import styleSheet from './LegalLinks.styles';
+
+const FooterLegalLinks = () => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const navigation = useNavigation();
+
+ const handleNavigateToWebView = (url: string) =>
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: { url },
+ });
+
+ return (
+
+
+ handleNavigateToWebView(AppConstants.URLS.TERMS_AND_CONDITIONS)
+ }
+ style={styles.legalLink}
+ >
+
+ {strings('stake.terms_of_service')}
+
+
+
+ handleNavigateToWebView(AppConstants.URLS.STAKING_RISK_DISCLOSURE)
+ }
+ style={styles.legalLink}
+ >
+
+ {strings('stake.risk_disclosure')}
+
+
+
+ );
+};
+
+export default FooterLegalLinks;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/__snapshots__/LegalLinks.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/__snapshots__/LegalLinks.test.tsx.snap
new file mode 100644
index 00000000000..2fc6328066f
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/LegalLinks/__snapshots__/LegalLinks.test.tsx.snap
@@ -0,0 +1,187 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FooterLegalLinks navigates to risk disclosure web page 1`] = `
+
+
+
+ Terms of service
+
+
+
+
+ Risk disclosure
+
+
+
+`;
+
+exports[`FooterLegalLinks navigates to terms of use web page 1`] = `
+
+
+
+ Terms of service
+
+
+
+
+ Risk disclosure
+
+
+
+`;
+
+exports[`FooterLegalLinks render matches snapshot 1`] = `
+
+
+
+ Terms of service
+
+
+
+
+ Risk disclosure
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/__snapshots__/ConfirmationFooter.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/__snapshots__/ConfirmationFooter.test.tsx.snap
new file mode 100644
index 00000000000..e29a06f61c9
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/__snapshots__/ConfirmationFooter.test.tsx.snap
@@ -0,0 +1,162 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ConfirmationFooter render matches snapshot 1`] = `
+
+
+
+
+ Terms of service
+
+
+
+
+ Risk disclosure
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ Confirm
+
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.test.tsx
new file mode 100644
index 00000000000..c88f586fb98
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.test.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import ContractTag from './ContractTag';
+
+const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking';
+
+describe('ContractTag', () => {
+ it('render matches snapshot', () => {
+ const { getByText, toJSON } = renderWithProvider(
+ ,
+ );
+
+ expect(getByText(MOCK_STAKING_CONTRACT_NAME)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.tsx b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.tsx
new file mode 100644
index 00000000000..4b3eb3cee57
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import TagBase, {
+ TagSeverity,
+ TagShape,
+} from '../../../../../../component-library/base-components/TagBase';
+import Icon, {
+ IconName,
+ IconSize,
+} from '../../../../../../component-library/components/Icons/Icon';
+import Text from '../../../../../../component-library/components/Texts/Text';
+import { ContractTagProps } from './ContractTag.types';
+
+const ContractTag = ({ contractName }: ContractTagProps) => (
+ }
+ shape={TagShape.Pill}
+ severity={TagSeverity.Neutral}
+ >
+ {contractName}
+
+);
+
+export default ContractTag;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.types.ts b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.types.ts
new file mode 100644
index 00000000000..082888ac193
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/ContractTag.types.ts
@@ -0,0 +1,3 @@
+export interface ContractTagProps {
+ contractName: string;
+}
diff --git a/app/components/UI/Stake/components/StakingConfirmation/ContractTag/__snapshots__/ContractTag.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/__snapshots__/ContractTag.test.tsx.snap
new file mode 100644
index 00000000000..bd7e17af2f8
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/ContractTag/__snapshots__/ContractTag.test.tsx.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ContractTag render matches snapshot 1`] = `
+
+
+
+
+
+ MM Pooled Staking
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.styles.ts
new file mode 100644
index 00000000000..33c5ad186e8
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.styles.ts
@@ -0,0 +1,39 @@
+import type { Theme } from '../../../../../../util/theme/models';
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = (params: { theme: Theme }) => {
+ const { theme } = params;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ estGasFeeCard: {
+ borderWidth: 0,
+ gap: 16,
+ borderRadius: 8,
+ justifyContent: 'center',
+ },
+ // Estimated Gas Fee
+ estGasFeeValue: {
+ flexDirection: 'row',
+ paddingTop: 1,
+ },
+ foxIcon: {
+ paddingRight: 8,
+ },
+ fiatText: {
+ paddingRight: 4,
+ },
+ ethText: {
+ borderBottomWidth: 1,
+ borderBottomColor: colors.primary.default,
+ },
+ estimatedGasTooltipContent: {
+ gap: 16,
+ },
+ gasLearnMoreLink: {
+ alignSelf: 'flex-start',
+ },
+ });
+};
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.test.tsx
new file mode 100644
index 00000000000..97a31da76f3
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.test.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import EstimatedGasCard from './EstimatedGasCard';
+import { strings } from '../../../../../../../locales/i18n';
+import { fireEvent } from '@testing-library/react-native';
+
+const mockNavigate = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ }),
+ };
+});
+
+describe('EstimatedGasCard', () => {
+ it('render matches snapshot', () => {
+ const props = {
+ gasCostEth: '0.0884 ETH',
+ gasCostFiat: '$43.56',
+ };
+
+ const { getByText, toJSON } = renderWithProvider(
+ ,
+ );
+
+ expect(
+ getByText(strings('tooltip_modal.estimated_gas_fee.title')),
+ ).toBeDefined();
+ expect(getByText(props.gasCostEth)).toBeDefined();
+ expect(getByText(props.gasCostFiat)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('displays the estimated gas cost tooltip when pressed', () => {
+ const props = {
+ gasCostEth: '0.0884 ETH',
+ gasCostFiat: '$43.56',
+ };
+
+ const { getByLabelText, toJSON } = renderWithProvider(
+ ,
+ );
+
+ fireEvent.press(
+ getByLabelText(
+ `${strings('tooltip_modal.estimated_gas_fee.title')} tooltip`,
+ ),
+ );
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith('RootModalFlow', {
+ params: {
+ title: strings('tooltip_modal.estimated_gas_fee.title'),
+ // difficult to directly compare ReactNodes
+ tooltip: expect.any(Object),
+ },
+ screen: 'tooltipModal',
+ });
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.tsx b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.tsx
new file mode 100644
index 00000000000..d8d9c83130e
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { View, TouchableOpacity } from 'react-native';
+import { strings } from '../../../../../../../locales/i18n';
+import KeyValueRow, {
+ TooltipSizes,
+} from '../../../../../../component-library/components-temp/KeyValueRow';
+import Text, {
+ TextColor,
+ TextVariant,
+} from '../../../../../../component-library/components/Texts/Text';
+import Card from '../../../../../../component-library/components/Cards/Card';
+import { useStyles } from '../../../../../hooks/useStyles';
+import useTooltipModal from '../../../../../hooks/useTooltipModal';
+import styleSheet from './EstimatedGasCard.styles';
+import { EstimatedGasCardProps } from './EstimatedGasCard.types';
+import EstimatedGasFeeTooltipContent from '../EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent';
+
+const EstimatedGasCard = ({
+ gasCostEth,
+ gasCostFiat,
+}: EstimatedGasCardProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const { openTooltipModal } = useTooltipModal();
+
+ // TODO: Navigate to the edit gas bottom sheet
+ const handleNavigateToEditGas = () =>
+ openTooltipModal('TODO', 'Navigate to gas customization component');
+
+ return (
+
+ ,
+ size: TooltipSizes.Sm,
+ },
+ }}
+ value={{
+ label: (
+
+ 🦊
+
+ {gasCostFiat}
+
+
+
+
+ {gasCostEth}
+
+
+
+
+ ),
+ }}
+ />
+
+ );
+};
+
+export default EstimatedGasCard;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.types.ts b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.types.ts
new file mode 100644
index 00000000000..58cd5f1aed7
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/EstimatedGasCard.types.ts
@@ -0,0 +1,4 @@
+export interface EstimatedGasCardProps {
+ gasCostEth: string;
+ gasCostFiat: string;
+}
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/__snapshots__/EstimatedGasCard.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/__snapshots__/EstimatedGasCard.test.tsx.snap
new file mode 100644
index 00000000000..3bb66a34c5c
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasCard/__snapshots__/EstimatedGasCard.test.tsx.snap
@@ -0,0 +1,419 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EstimatedGasCard displays the estimated gas cost tooltip when pressed 1`] = `
+
+
+
+
+
+
+ Estimated gas fee
+
+
+
+
+
+
+
+
+
+
+
+
+ 🦊
+
+
+ $43.56
+
+
+
+
+ 0.0884 ETH
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`EstimatedGasCard render matches snapshot 1`] = `
+
+
+
+
+
+
+ Estimated gas fee
+
+
+
+
+
+
+
+
+
+
+
+
+ 🦊
+
+
+ $43.56
+
+
+
+
+ 0.0884 ETH
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.styles.ts
new file mode 100644
index 00000000000..de81ef5a0bd
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.styles.ts
@@ -0,0 +1,13 @@
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = () =>
+ StyleSheet.create({
+ estimatedGasTooltipContent: {
+ gap: 16,
+ },
+ gasLearnMoreLink: {
+ alignSelf: 'flex-start',
+ },
+ });
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.test.tsx
new file mode 100644
index 00000000000..440fb7f7f3b
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.test.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import EstimatedGasFeeTooltipContent from './EstimatedGasFeeTooltipContent';
+import { strings } from '../../../../../../../locales/i18n';
+import { fireEvent } from '@testing-library/react-native';
+
+const mockNavigate = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ }),
+ };
+});
+
+describe('EstimatedGasFeeTooltipContent', () => {
+ it('render matches snapshot', () => {
+ const { getByText, toJSON } = renderWithProvider(
+ ,
+ );
+
+ expect(
+ getByText(strings('tooltip_modal.estimated_gas_fee.gas_recipient')),
+ ).toBeDefined();
+ expect(
+ getByText(strings('tooltip_modal.estimated_gas_fee.gas_fluctuation')),
+ ).toBeDefined();
+ expect(
+ getByText(strings('tooltip_modal.estimated_gas_fee.gas_learn_more')),
+ ).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('redirects to "learn more about gas fees" web view when learn more pressed', () => {
+ const { getByText } = renderWithProvider();
+
+ fireEvent.press(
+ getByText(strings('tooltip_modal.estimated_gas_fee.gas_learn_more')),
+ );
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith('Webview', {
+ params: {
+ url: 'https://support.metamask.io/transactions-and-gas/gas-fees/why-are-my-gas-fees-so-high/',
+ },
+ screen: 'SimpleWebview',
+ });
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.tsx b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.tsx
new file mode 100644
index 00000000000..65a52044848
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/EstimatedGasFeeTooltipContent.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { useNavigation } from '@react-navigation/native';
+import { View, TouchableOpacity } from 'react-native';
+import { strings } from '../../../../../../../locales/i18n';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../../component-library/components/Texts/Text';
+import AppConstants from '../../../../../../core/AppConstants';
+import { useStyles } from '../../../../../hooks/useStyles';
+import styleSheet from './EstimatedGasFeeTooltipContent.styles';
+
+export const EstimatedGasFeeTooltipContent = () => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const navigation = useNavigation();
+
+ const handleNavigateToGasLearnMore = () =>
+ navigation.navigate('Webview', {
+ screen: 'SimpleWebview',
+ params: {
+ url: AppConstants.REVIEW_PROMPT.HIGH_GAS_FEES,
+ },
+ });
+
+ return (
+
+ {strings('tooltip_modal.estimated_gas_fee.gas_recipient')}
+ {strings('tooltip_modal.estimated_gas_fee.gas_fluctuation')}
+
+
+ {strings('tooltip_modal.estimated_gas_fee.gas_learn_more')}
+
+
+
+ );
+};
+
+export default EstimatedGasFeeTooltipContent;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/__snapshots__/EstimatedGasFeeTooltipContent.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/__snapshots__/EstimatedGasFeeTooltipContent.test.tsx.snap
new file mode 100644
index 00000000000..bd50772cb00
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/EstimatedGasFeeTooltipContent/__snapshots__/EstimatedGasFeeTooltipContent.test.tsx.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EstimatedGasFeeTooltipContent render matches snapshot 1`] = `
+
+
+ Gas fees are paid to crypto miners who process transactions on Ethereum network. Metamask does not profit from gas fees.
+
+
+ Gas fees are estimated and will fluctuate based on network traffic and transaction complexity.
+
+
+
+ Learn more about gas fees
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.styles.ts
new file mode 100644
index 00000000000..85e2cd913df
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.styles.ts
@@ -0,0 +1,16 @@
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = () =>
+ StyleSheet.create({
+ card: {
+ borderWidth: 0,
+ gap: 16,
+ borderRadius: 8,
+ },
+ estAnnualRewardValue: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ });
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.test.tsx
new file mode 100644
index 00000000000..ae8bebfe465
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.test.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import RewardsCard from './RewardsCard';
+import { RewardsCardProps } from './RewardsCard.types';
+import { fireEvent } from '@testing-library/react-native';
+import { strings } from '../../../../../../../locales/i18n';
+
+const mockNavigate = jest.fn();
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ }),
+ };
+});
+
+describe('RewardsCard', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('render matches snapshot', () => {
+ const props: RewardsCardProps = {
+ rewardRate: '2.6',
+ rewardsEth: '0.13 ETH',
+ rewardsFiat: '$334.93',
+ };
+
+ const { getByText, toJSON } = renderWithProvider(
+ ,
+ );
+
+ expect(getByText(`${props.rewardRate}%`)).toBeDefined();
+ expect(getByText(props.rewardsEth)).toBeDefined();
+ expect(getByText(props.rewardsFiat)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('reward rate tooltip displayed when pressed', () => {
+ const props: RewardsCardProps = {
+ rewardRate: '2.6',
+ rewardsEth: '0.13 ETH',
+ rewardsFiat: '$334.93',
+ };
+
+ const { toJSON, getByLabelText } = renderWithProvider(
+ ,
+ );
+
+ fireEvent.press(
+ getByLabelText(`${strings('tooltip_modal.reward_rate.title')} tooltip`),
+ );
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith('RootModalFlow', {
+ params: {
+ title: strings('tooltip_modal.reward_rate.title'),
+ tooltip: strings('tooltip_modal.reward_rate.tooltip'),
+ },
+ screen: 'tooltipModal',
+ });
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('reward frequency tooltip displayed when pressed', () => {
+ const props: RewardsCardProps = {
+ rewardRate: '2.6',
+ rewardsEth: '0.13 ETH',
+ rewardsFiat: '$334.93',
+ };
+
+ const { toJSON, getByLabelText } = renderWithProvider(
+ ,
+ );
+
+ fireEvent.press(
+ getByLabelText(
+ `${strings('tooltip_modal.reward_frequency.title')} tooltip`,
+ ),
+ );
+
+ expect(mockNavigate).toHaveBeenCalledTimes(1);
+ expect(mockNavigate).toHaveBeenCalledWith('RootModalFlow', {
+ params: {
+ title: strings('tooltip_modal.reward_frequency.title'),
+ tooltip: strings('tooltip_modal.reward_frequency.tooltip'),
+ },
+ screen: 'tooltipModal',
+ });
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.tsx b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.tsx
new file mode 100644
index 00000000000..66b6eacedf5
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { View } from 'react-native';
+import { strings } from '../../../../../../../locales/i18n';
+import KeyValueRow, {
+ TooltipSizes,
+} from '../../../../../../component-library/components-temp/KeyValueRow';
+import Text, {
+ TextColor,
+ TextVariant,
+} from '../../../../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../../../hooks/useStyles';
+import Card from '../../../../../../component-library/components/Cards/Card';
+import styleSheet from './RewardsCard.styles';
+import { RewardsCardProps } from './RewardsCard.types';
+import { fixDisplayAmount } from '../../../utils/value';
+
+const RewardsCard = ({
+ rewardRate,
+ rewardsEth,
+ rewardsFiat,
+}: RewardsCardProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ return (
+
+
+
+ {rewardsFiat}
+ {rewardsEth}
+
+ ),
+ }}
+ />
+
+
+ );
+};
+
+export default RewardsCard;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.types.ts b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.types.ts
new file mode 100644
index 00000000000..3d60d99f371
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.types.ts
@@ -0,0 +1,5 @@
+export interface RewardsCardProps {
+ rewardRate: string;
+ rewardsEth: string;
+ rewardsFiat: string;
+}
diff --git a/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/__snapshots__/RewardsCard.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/__snapshots__/RewardsCard.test.tsx.snap
new file mode 100644
index 00000000000..e2ace2593d4
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/__snapshots__/RewardsCard.test.tsx.snap
@@ -0,0 +1,1255 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RewardsCard render matches snapshot 1`] = `
+
+
+
+
+
+
+ Reward rate
+
+
+
+
+
+
+
+
+
+
+
+ 2.6%
+
+
+
+
+
+
+
+
+
+
+ Estimated annual rewards
+
+
+
+
+
+
+
+
+
+ $334.93
+
+
+ 0.13 ETH
+
+
+
+
+
+
+
+
+
+
+
+ Reward frequency
+
+
+
+
+
+
+
+
+
+
+
+ 12 hours
+
+
+
+
+
+
+`;
+
+exports[`RewardsCard reward frequency tooltip displayed when pressed 1`] = `
+
+
+
+
+
+
+ Reward rate
+
+
+
+
+
+
+
+
+
+
+
+ 2.6%
+
+
+
+
+
+
+
+
+
+
+ Estimated annual rewards
+
+
+
+
+
+
+
+
+
+ $334.93
+
+
+ 0.13 ETH
+
+
+
+
+
+
+
+
+
+
+
+ Reward frequency
+
+
+
+
+
+
+
+
+
+
+
+ 12 hours
+
+
+
+
+
+
+`;
+
+exports[`RewardsCard reward rate tooltip displayed when pressed 1`] = `
+
+
+
+
+
+
+ Reward rate
+
+
+
+
+
+
+
+
+
+
+
+ 2.6%
+
+
+
+
+
+
+
+
+
+
+ Estimated annual rewards
+
+
+
+
+
+
+
+
+
+ $334.93
+
+
+ 0.13 ETH
+
+
+
+
+
+
+
+
+
+
+
+ Reward frequency
+
+
+
+
+
+
+
+
+
+
+
+ 12 hours
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.styles.ts b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.styles.ts
new file mode 100644
index 00000000000..36e6e22593b
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.styles.ts
@@ -0,0 +1,23 @@
+import { StyleSheet } from 'react-native';
+
+const stylesSheet = () =>
+ StyleSheet.create({
+ tokenValueStackContainer: {
+ alignItems: 'center',
+ paddingVertical: 8,
+ gap: 8,
+ },
+ badgeWrapper: {
+ alignSelf: 'center',
+ },
+ ethLogo: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ },
+ balancesContainer: {
+ alignItems: 'center',
+ },
+ });
+
+export default stylesSheet;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.test.tsx
new file mode 100644
index 00000000000..bcc0060c555
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.test.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import TokenValueStack from './TokenValueStack';
+import { Image } from 'react-native';
+import { TokenValueStackProps } from './TokenValueStack.types';
+import { renderFromWei } from '../../../../../../util/number';
+
+jest.mock('../../../../../hooks/useIpfsGateway', () => jest.fn());
+
+Image.getSize = jest.fn((_uri, success) => {
+ success(100, 100); // Mock successful response for ETH native Icon Image
+});
+
+describe('TokenValueStack', () => {
+ it('render matches snapshot', () => {
+ const props: TokenValueStackProps = {
+ amountWei: '3210000000000000',
+ amountFiat: '7.46',
+ tokenSymbol: 'wETH',
+ };
+
+ const { getByText, toJSON } = renderWithProvider(
+ ,
+ );
+
+ expect(
+ getByText(`${renderFromWei(props.amountWei)} ${props.tokenSymbol}`),
+ ).toBeDefined(); // 0.00321 wETH
+ expect(getByText(props.amountFiat)).toBeDefined();
+
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.tsx b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.tsx
new file mode 100644
index 00000000000..8d98a2a05fb
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { View } from 'react-native';
+import { useSelector } from 'react-redux';
+import Badge, {
+ BadgeVariant,
+} from '../../../../../../component-library/components/Badges/Badge';
+import BadgeWrapper from '../../../../../../component-library/components/Badges/BadgeWrapper';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../../../component-library/components/Texts/Text';
+import { selectNetworkName } from '../../../../../../selectors/networkInfos';
+import { useStyles } from '../../../../../hooks/useStyles';
+import NetworkMainAssetLogo from '../../../../NetworkMainAssetLogo';
+import styleSheet from './TokenValueStack.styles';
+import images from '../../../../../../images/image-icons';
+import { TokenValueStackProps } from './TokenValueStack.types';
+import { renderFromWei } from '../../../../../../util/number';
+
+const TokenValueStack = ({
+ amountWei,
+ amountFiat,
+ tokenSymbol,
+ style,
+}: TokenValueStackProps) => {
+ const { styles } = useStyles(styleSheet, {});
+
+ const networkName = useSelector(selectNetworkName);
+
+ return (
+
+
+ }
+ >
+
+
+
+
+ {renderFromWei(amountWei)} {tokenSymbol}
+
+ {amountFiat}
+
+
+ );
+};
+
+export default TokenValueStack;
diff --git a/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.types.ts b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.types.ts
new file mode 100644
index 00000000000..add4a7ba379
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/TokenValueStack.types.ts
@@ -0,0 +1,7 @@
+import { ViewProps } from 'react-native';
+
+export interface TokenValueStackProps extends Pick {
+ amountWei: string;
+ amountFiat: string;
+ tokenSymbol: string;
+}
diff --git a/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/__snapshots__/TokenValueStack.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/__snapshots__/TokenValueStack.test.tsx.snap
new file mode 100644
index 00000000000..32f3cf0ccfc
--- /dev/null
+++ b/app/components/UI/Stake/components/StakingConfirmation/TokenValueStack/__snapshots__/TokenValueStack.test.tsx.snap
@@ -0,0 +1,212 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TokenValueStack render matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0.00321
+
+ wETH
+
+
+ 7.46
+
+
+
+`;
diff --git a/app/components/UI/Stake/routes/index.tsx b/app/components/UI/Stake/routes/index.tsx
index 321d0eb7f1c..14993705ca5 100644
--- a/app/components/UI/Stake/routes/index.tsx
+++ b/app/components/UI/Stake/routes/index.tsx
@@ -3,6 +3,7 @@ import { createStackNavigator } from '@react-navigation/stack';
import StakeInputView from '../Views/StakeInputView/StakeInputView';
import LearnMoreModal from '../components/LearnMoreModal';
import Routes from '../../../../constants/navigation/Routes';
+import StakeConfirmationView from '../Views/StakeConfirmationView/StakeConfirmationView';
import UnstakeInputView from '../Views/UnstakeInputView/UnstakeInputView';
const Stack = createStackNavigator();
const ModalStack = createStackNavigator();
@@ -20,6 +21,10 @@ const StakeScreenStack = () => (
+
);
diff --git a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx
index a836606c818..7ab0ad470d5 100644
--- a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx
+++ b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx
@@ -47,6 +47,7 @@ import { strings } from '../../../../../../locales/i18n';
import { ScamWarningIcon } from '../ScamWarningIcon';
import { ScamWarningModal } from '../ScamWarningModal';
import { StakeButton } from '../StakeButton';
+import { CustomNetworkImgMapping } from '../../../../../util/networks/customNetworks';
interface TokenListItemProps {
asset: TokenI;
@@ -157,6 +158,10 @@ export const TokenListItem = ({
if (isLineaMainnet) return images['LINEA-MAINNET'];
+ if (CustomNetworkImgMapping[chainId]) {
+ return CustomNetworkImgMapping[chainId];
+ }
+
return ticker ? images[ticker] : undefined;
};
diff --git a/app/components/Views/LockScreen/index.js b/app/components/Views/LockScreen/index.js
index 92f04193389..030bc9ace1d 100644
--- a/app/components/Views/LockScreen/index.js
+++ b/app/components/Views/LockScreen/index.js
@@ -22,6 +22,7 @@ import {
import Routes from '../../../constants/navigation/Routes';
import { CommonActions } from '@react-navigation/native';
import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics';
+import { trace, TraceName, TraceOperation } from '../../../util/trace';
const LOGO_SIZE = 175;
const createStyles = (colors) =>
@@ -134,10 +135,19 @@ class LockScreen extends PureComponent {
// Retrieve the credentials
Logger.log('Lockscreen::unlockKeychain - getting credentials');
- await Authentication.appTriggeredAuth({
- bioStateMachineId,
- disableAutoLogout: true,
- });
+ await trace(
+ {
+ name: TraceName.BiometricAuthentication,
+ op: TraceOperation.BiometricAuthentication,
+ },
+ async () => {
+ await Authentication.appTriggeredAuth({
+ bioStateMachineId,
+ disableAutoLogout: true,
+ });
+ },
+ );
+
this.setState({ ready: true });
Logger.log('Lockscreen::unlockKeychain - state: ready');
} catch (error) {
diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js
index 488804f7a36..e737df72178 100644
--- a/app/components/Views/Login/index.js
+++ b/app/components/Views/Login/index.js
@@ -58,6 +58,12 @@ import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selector
import { withMetricsAwareness } from '../../../components/hooks/useMetrics';
import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics';
import { downloadStateLogs } from '../../../util/logs';
+import {
+ trace,
+ endTrace,
+ TraceName,
+ TraceOperation,
+} from '../../../util/trace';
const deviceHeight = Device.getDeviceHeight();
const breakPoint = deviceHeight < 700;
@@ -244,6 +250,10 @@ class Login extends PureComponent {
fieldRef = React.createRef();
async componentDidMount() {
+ trace({
+ name: TraceName.LoginToPasswordEntry,
+ op: TraceOperation.LoginToPasswordEntry,
+ });
this.props.metrics.trackEvent(MetaMetricsEvents.LOGIN_SCREEN_VIEWED);
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
@@ -367,7 +377,15 @@ class Login extends PureComponent {
);
try {
- await Authentication.userEntryAuth(password, authType);
+ await trace(
+ {
+ name: TraceName.AuthenticateUser,
+ op: TraceOperation.AuthenticateUser,
+ },
+ async () => {
+ await Authentication.userEntryAuth(password, authType);
+ },
+ );
Keyboard.dismiss();
@@ -435,7 +453,15 @@ class Login extends PureComponent {
const { current: field } = this.fieldRef;
field?.blur();
try {
- await Authentication.appTriggeredAuth();
+ await trace(
+ {
+ name: TraceName.BiometricAuthentication,
+ op: TraceOperation.BiometricAuthentication,
+ },
+ async () => {
+ await Authentication.appTriggeredAuth();
+ },
+ );
const onboardingWizard = await StorageWrapper.getItem(ONBOARDING_WIZARD);
if (!onboardingWizard) this.props.setOnboardingWizardStep(1);
this.props.navigation.replace(Routes.ONBOARDING.HOME_NAV);
@@ -454,6 +480,7 @@ class Login extends PureComponent {
};
triggerLogIn = () => {
+ endTrace({ name: TraceName.LoginToPasswordEntry });
this.onLogin();
};
@@ -536,10 +563,7 @@ class Login extends PureComponent {
)}
-
+
{strings('login.title')}
diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js
index f15aa33c3c5..52ea24f6192 100644
--- a/app/components/Views/Onboarding/index.js
+++ b/app/components/Views/Onboarding/index.js
@@ -49,6 +49,7 @@ import { OnboardingSelectorIDs } from '../../../../e2e/selectors/Onboarding/Onbo
import Routes from '../../../constants/navigation/Routes';
import { selectAccounts } from '../../../selectors/accountTrackerController';
import trackOnboarding from '../../../util/metrics/TrackOnboarding/trackOnboarding';
+import { trace, TraceName, TraceOperation } from '../../../util/trace';
const createStyles = (colors) =>
StyleSheet.create({
@@ -275,24 +276,33 @@ class Onboarding extends PureComponent {
};
onPressCreate = () => {
- const action = async () => {
- const { metrics } = this.props;
- if (metrics.isEnabled()) {
- this.props.navigation.navigate('ChoosePassword', {
- [PREVIOUS_SCREEN]: ONBOARDING,
- });
- this.track(MetaMetricsEvents.WALLET_SETUP_STARTED);
- } else {
- this.props.navigation.navigate('OptinMetrics', {
- onContinue: () => {
- this.props.navigation.replace('ChoosePassword', {
+ const action = () => {
+ trace(
+ {
+ name: TraceName.CreateNewWalletToChoosePassword,
+ op: TraceOperation.CreateNewWalletToChoosePassword,
+ },
+ () => {
+ const { metrics } = this.props;
+ if (metrics.isEnabled()) {
+ this.props.navigation.navigate('ChoosePassword', {
[PREVIOUS_SCREEN]: ONBOARDING,
});
this.track(MetaMetricsEvents.WALLET_SETUP_STARTED);
- },
- });
- }
+ } else {
+ this.props.navigation.navigate('OptinMetrics', {
+ onContinue: () => {
+ this.props.navigation.replace('ChoosePassword', {
+ [PREVIOUS_SCREEN]: ONBOARDING,
+ });
+ this.track(MetaMetricsEvents.WALLET_SETUP_STARTED);
+ },
+ });
+ }
+ },
+ );
};
+
this.handleExistingUser(action);
};
diff --git a/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap
index 1cde78a543d..1859f635317 100644
--- a/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/NotificationsSettings/__snapshots__/index.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`NotificationsSettings should render correctly 1`] = `
+exports[`NotificationsSettings render matches snapshot 1`] = `
{
mockGetState = jest.fn();
@@ -57,130 +50,10 @@ jest.mock('../../../../util/notifications/services/NotificationService', () => (
getAllPermissions: jest.fn(),
}));
-jest.mock('../../../../core/Analytics/MetaMetrics.events', () => ({
- MetaMetricsEvents: {
- NOTIFICATIONS_SETTINGS_UPDATED: 'NOTIFICATIONS_SETTINGS_UPDATED',
- },
-}));
-
-const mockDisableNotifications = jest.fn();
-const mockEnableNotifications = jest.fn();
-const mockSetUiNotificationStatus = jest.fn();
-const mockTrackEvent = jest.fn();
-
-const mockNavigation = {
- navigate: jest.fn(),
-} as unknown as NavigationProp;
-
const setOptions = jest.fn();
-describe('toggleNotificationsEnabled', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- const setup = (basicFunctionalityEnabled: boolean, isMetamaskNotificationsEnabled: boolean, isProfileSyncingEnabled: boolean) => renderHook(() =>
- useCallback(async () => {
- if (!basicFunctionalityEnabled) {
- mockNavigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.BASIC_FUNCTIONALITY,
- params: {
- caller: Routes.SETTINGS.NOTIFICATIONS,
- },
- });
- } else if (isMetamaskNotificationsEnabled) {
- mockDisableNotifications();
- mockSetUiNotificationStatus(false);
- } else {
- const { permission } = await NotificationsService.getAllPermissions(false);
- if (permission !== 'authorized') {
- return;
- }
-
- mockEnableNotifications();
- mockSetUiNotificationStatus(true);
- }
- mockTrackEvent(MetaMetricsEvents.NOTIFICATIONS_SETTINGS_UPDATED, {
- settings_type: 'notifications',
- old_value: isMetamaskNotificationsEnabled,
- new_value: !isMetamaskNotificationsEnabled,
- was_profile_syncing_on: isMetamaskNotificationsEnabled ? true : isProfileSyncingEnabled,
- });
- }, [])
- );
-
- it('should navigate to basic functionality screen if basicFunctionalityEnabled is false', async () => {
- const { result } = setup(false, false, false);
-
- await act(async () => {
- await result.current();
- });
-
- expect(mockNavigation.navigate).toHaveBeenCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.BASIC_FUNCTIONALITY,
- params: {
- caller: Routes.SETTINGS.NOTIFICATIONS,
- },
- });
- });
-
- it('should disable notifications if isMetamaskNotificationsEnabled is true', async () => {
- const { result } = setup(true, true, false);
-
- await act(async () => {
- await result.current();
- });
-
- expect(mockDisableNotifications).toHaveBeenCalled();
- expect(mockSetUiNotificationStatus).toHaveBeenCalledWith(false);
- });
-
- it('should enable notifications if isMetamaskNotificationsEnabled is false and permission is authorized', async () => {
- (NotificationsService.getAllPermissions as jest.Mock).mockResolvedValue({ permission: 'authorized' });
-
- const { result } = setup(true, false, false);
-
- await act(async () => {
- await result.current();
- });
-
- expect(mockEnableNotifications).toHaveBeenCalled();
- expect(mockSetUiNotificationStatus).toHaveBeenCalledWith(true);
- });
-
- it('should not enable notifications if permission is not authorized', async () => {
- (NotificationsService.getAllPermissions as jest.Mock).mockResolvedValue({ permission: 'denied' });
-
- const { result } = setup(true, false, false);
-
- await act(async () => {
- await result.current();
- });
-
- expect(mockEnableNotifications).not.toHaveBeenCalled();
- expect(mockSetUiNotificationStatus).not.toHaveBeenCalled();
- });
-
- it('should track event when notifications settings are updated', async () => {
- (NotificationsService.getAllPermissions as jest.Mock).mockResolvedValue({ permission: 'authorized' });
-
- const { result } = setup(true, false, true);
-
- await act(async () => {
- await result.current();
- });
-
- expect(mockTrackEvent).toHaveBeenCalledWith(MetaMetricsEvents.NOTIFICATIONS_SETTINGS_UPDATED, {
- settings_type: 'notifications',
- old_value: false,
- new_value: true,
- was_profile_syncing_on: true,
- });
- });
-});
-
describe('NotificationsSettings', () => {
- it('should render correctly', () => {
+ it('render matches snapshot', () => {
mockGetState.mockImplementation(() => ({
notifications: {},
}));
@@ -199,21 +72,4 @@ describe('NotificationsSettings', () => {
);
expect(toJSON()).toMatchSnapshot();
});
-
- it('should toggle notifications and handle permission correctly', async () => {
- const isMetamaskNotificationsEnabled = true;
- const basicFunctionalityEnabled = true;
- const isProfileSyncingEnabled = true;
-
- const toggleNotificationsEnabledImpl = jest.fn(() => Promise.resolve({
- isMetamaskNotificationsEnabled,
- basicFunctionalityEnabled,
- isProfileSyncingEnabled,
- }));
-
- await toggleNotificationsEnabledImpl();
-
- expect(NotificationsService.getAllPermissions).toHaveBeenCalledTimes(1);
- expect(NotificationsService.getAllPermissions).toHaveBeenCalledWith(false);
- });
});
diff --git a/app/components/Views/Settings/NotificationsSettings/index.tsx b/app/components/Views/Settings/NotificationsSettings/index.tsx
index 34e9da18047..e046b203c2e 100644
--- a/app/components/Views/Settings/NotificationsSettings/index.tsx
+++ b/app/components/Views/Settings/NotificationsSettings/index.tsx
@@ -31,7 +31,6 @@ import {
selectIsProfileSyncingEnabled,
} from '../../../../selectors/notifications';
-import NotificationsService from '../../../../util/notifications/services/NotificationService';
import Routes from '../../../../constants/navigation/Routes';
import ButtonIcon, {
@@ -57,6 +56,7 @@ import AppConstants from '../../../../core/AppConstants';
import notificationsRows from './notificationsRows';
import { IconName } from '../../../../component-library/components/Icons/Icon';
import { MetaMetricsEvents } from '../../../../core/Analytics/MetaMetrics.events';
+import { useToggleNotifications } from './useToggleNotifications';
interface MainNotificationSettingsProps extends Props {
toggleNotificationsEnabled: () => void;
@@ -109,6 +109,7 @@ const NotificationsSettings = ({ navigation, route }: Props) => {
const { accounts } = useAccounts();
const { trackEvent } = useMetrics();
const theme = useTheme();
+
const isMetamaskNotificationsEnabled = useSelector(
selectIsMetamaskNotificationsEnabled,
);
@@ -177,42 +178,15 @@ const NotificationsSettings = ({ navigation, route }: Props) => {
* it will request the push notifications permission and enable the notifications
* if the permission is granted.
*/
- const toggleNotificationsEnabled = useCallback(async () => {
- if (!basicFunctionalityEnabled) {
- navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
- screen: Routes.SHEET.BASIC_FUNCTIONALITY,
- params: {
- caller: Routes.SETTINGS.NOTIFICATIONS,
- },
- });
- } else if (isMetamaskNotificationsEnabled) {
- disableNotifications();
- setUiNotificationStatus(false);
- } else {
- const { permission } = await NotificationsService.getAllPermissions(false);
- if (permission !== 'authorized') {
- return;
- }
- enableNotifications();
- setUiNotificationStatus(true);
- }
- trackEvent(MetaMetricsEvents.NOTIFICATIONS_SETTINGS_UPDATED, {
- settings_type: 'notifications',
- old_value: isMetamaskNotificationsEnabled,
- new_value: !isMetamaskNotificationsEnabled,
- was_profile_syncing_on: isMetamaskNotificationsEnabled
- ? true
- : isProfileSyncingEnabled,
- });
- }, [
+ const { toggleNotificationsEnabled } = useToggleNotifications({
+ navigation,
basicFunctionalityEnabled,
- disableNotifications,
- enableNotifications,
isMetamaskNotificationsEnabled,
- navigation,
- trackEvent,
isProfileSyncingEnabled,
- ]);
+ disableNotifications,
+ enableNotifications,
+ setUiNotificationStatus,
+ });
const toggleCustomNotificationsEnabled = useCallback(async () => {
setPlatformAnnouncementsState(!platformAnnouncementsState);
diff --git a/app/components/Views/Settings/NotificationsSettings/useToggleNotifications.test.tsx b/app/components/Views/Settings/NotificationsSettings/useToggleNotifications.test.tsx
new file mode 100644
index 00000000000..54c7addc69b
--- /dev/null
+++ b/app/components/Views/Settings/NotificationsSettings/useToggleNotifications.test.tsx
@@ -0,0 +1,125 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useToggleNotifications } from './useToggleNotifications';
+import NotificationsService from '../../../../util/notifications/services/NotificationService';
+import Routes from '../../../../constants/navigation/Routes';
+import { NavigationProp, ParamListBase } from '@react-navigation/native';
+
+jest.mock(
+ '../../../../util/notifications/services/NotificationService',
+ () => ({
+ getAllPermissions: jest.fn(),
+ }),
+);
+
+const mockNavigation = {
+ navigate: jest.fn(),
+} as unknown as NavigationProp;
+
+const mockDisableNotifications = jest.fn();
+const mockEnableNotifications = jest.fn();
+const mockSetUiNotificationStatus = jest.fn();
+
+describe('useToggleNotifications', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('navigates to basic functionality screen if basic functionality is disabled', async () => {
+ const { result } = renderHook(() =>
+ useToggleNotifications({
+ navigation: mockNavigation,
+ basicFunctionalityEnabled: false,
+ isMetamaskNotificationsEnabled: false,
+ isProfileSyncingEnabled: false,
+ disableNotifications: mockDisableNotifications,
+ enableNotifications: mockEnableNotifications,
+ setUiNotificationStatus: mockSetUiNotificationStatus,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.toggleNotificationsEnabled();
+ });
+
+ expect(mockNavigation.navigate).toHaveBeenCalledWith(
+ Routes.MODAL.ROOT_MODAL_FLOW,
+ {
+ screen: Routes.SHEET.BASIC_FUNCTIONALITY,
+ params: {
+ caller: Routes.SETTINGS.NOTIFICATIONS,
+ },
+ },
+ );
+ });
+
+ it('switches notifications OFF if notifications previously enabled', async () => {
+ const { result } = renderHook(() =>
+ useToggleNotifications({
+ navigation: mockNavigation,
+ basicFunctionalityEnabled: true,
+ isMetamaskNotificationsEnabled: true,
+ isProfileSyncingEnabled: false,
+ disableNotifications: mockDisableNotifications,
+ enableNotifications: mockEnableNotifications,
+ setUiNotificationStatus: mockSetUiNotificationStatus,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.toggleNotificationsEnabled();
+ });
+
+ expect(mockDisableNotifications).toHaveBeenCalled();
+ expect(mockSetUiNotificationStatus).toHaveBeenCalledWith(false);
+ });
+
+ it('switches notifications ON if notifications previously disabled and permission is authorized', async () => {
+ (NotificationsService.getAllPermissions as jest.Mock).mockResolvedValue({
+ permission: 'authorized',
+ });
+
+ const { result } = renderHook(() =>
+ useToggleNotifications({
+ navigation: mockNavigation,
+ basicFunctionalityEnabled: true,
+ isMetamaskNotificationsEnabled: false,
+ isProfileSyncingEnabled: false,
+ disableNotifications: mockDisableNotifications,
+ enableNotifications: mockEnableNotifications,
+ setUiNotificationStatus: mockSetUiNotificationStatus,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.toggleNotificationsEnabled();
+ });
+
+ expect(mockEnableNotifications).toHaveBeenCalled();
+ expect(mockSetUiNotificationStatus).toHaveBeenCalledWith(true);
+ });
+
+ it('switches notifications OFF if device permission is not authorized', async () => {
+ (NotificationsService.getAllPermissions as jest.Mock).mockResolvedValue({
+ permission: 'denied',
+ });
+
+ const { result } = renderHook(() =>
+ useToggleNotifications({
+ navigation: mockNavigation,
+ basicFunctionalityEnabled: true,
+ isMetamaskNotificationsEnabled: false,
+ isProfileSyncingEnabled: false,
+ disableNotifications: mockDisableNotifications,
+ enableNotifications: mockEnableNotifications,
+ setUiNotificationStatus: mockSetUiNotificationStatus,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.toggleNotificationsEnabled();
+ });
+
+ expect(mockEnableNotifications).not.toHaveBeenCalled();
+ expect(mockSetUiNotificationStatus).not.toHaveBeenCalled();
+ });
+});
diff --git a/app/components/Views/Settings/NotificationsSettings/useToggleNotifications.ts b/app/components/Views/Settings/NotificationsSettings/useToggleNotifications.ts
new file mode 100644
index 00000000000..f5fff6ee5b6
--- /dev/null
+++ b/app/components/Views/Settings/NotificationsSettings/useToggleNotifications.ts
@@ -0,0 +1,70 @@
+import { useCallback } from 'react';
+import { NavigationProp, ParamListBase } from '@react-navigation/native';
+import Routes from '../../../../constants/navigation/Routes';
+import NotificationsService from '../../../../util/notifications/services/NotificationService';
+import { MetaMetricsEvents } from '../../../../core/Analytics/MetaMetrics.events';
+import { useMetrics } from '../../../hooks/useMetrics';
+
+interface Props {
+ navigation: NavigationProp;
+ basicFunctionalityEnabled: boolean;
+ isMetamaskNotificationsEnabled: boolean;
+ isProfileSyncingEnabled: boolean | null;
+ disableNotifications: () => Promise;
+ enableNotifications: () => Promise;
+ setUiNotificationStatus: (status: boolean) => void;
+}
+
+export function useToggleNotifications({
+ navigation,
+ basicFunctionalityEnabled,
+ isMetamaskNotificationsEnabled,
+ isProfileSyncingEnabled,
+ disableNotifications,
+ enableNotifications,
+ setUiNotificationStatus,
+}: Props) {
+ const { trackEvent } = useMetrics();
+ const toggleNotificationsEnabled = useCallback(async () => {
+ if (!basicFunctionalityEnabled) {
+ navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+ screen: Routes.SHEET.BASIC_FUNCTIONALITY,
+ params: {
+ caller: Routes.SETTINGS.NOTIFICATIONS,
+ },
+ });
+ } else if (isMetamaskNotificationsEnabled) {
+ disableNotifications();
+ setUiNotificationStatus(false);
+ } else {
+ const { permission } = await NotificationsService.getAllPermissions(
+ false,
+ );
+ if (permission !== 'authorized') {
+ return;
+ }
+
+ enableNotifications();
+ setUiNotificationStatus(true);
+ }
+ trackEvent(MetaMetricsEvents.NOTIFICATIONS_SETTINGS_UPDATED, {
+ settings_type: 'notifications',
+ old_value: isMetamaskNotificationsEnabled,
+ new_value: !isMetamaskNotificationsEnabled,
+ was_profile_syncing_on: isMetamaskNotificationsEnabled
+ ? true
+ : isProfileSyncingEnabled,
+ });
+ }, [
+ basicFunctionalityEnabled,
+ isMetamaskNotificationsEnabled,
+ trackEvent,
+ isProfileSyncingEnabled,
+ navigation,
+ disableNotifications,
+ setUiNotificationStatus,
+ enableNotifications,
+ ]);
+
+ return { toggleNotificationsEnabled };
+}
diff --git a/app/components/Views/TooltipModal/ToolTipModal.styles.ts b/app/components/Views/TooltipModal/ToolTipModal.styles.ts
index ff89c01b839..15b9645e681 100644
--- a/app/components/Views/TooltipModal/ToolTipModal.styles.ts
+++ b/app/components/Views/TooltipModal/ToolTipModal.styles.ts
@@ -3,7 +3,6 @@ import { StyleSheet } from 'react-native';
const styleSheet = () =>
StyleSheet.create({
content: {
- paddingBottom: 16,
paddingHorizontal: 32,
flexDirection: 'row',
justifyContent: 'center',
diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx
index 97984c60b48..e8513940f7e 100644
--- a/app/components/Views/Wallet/index.tsx
+++ b/app/components/Views/Wallet/index.tsx
@@ -104,6 +104,7 @@ import {
import { ButtonVariants } from '../../../component-library/components/Buttons/Button';
import { useListNotifications } from '../../../util/notifications/hooks/useNotifications';
import { isObject } from 'lodash';
+
const createStyles = ({ colors, typography }: Theme) =>
StyleSheet.create({
base: {
diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx
index a4750a7f191..860b145103e 100644
--- a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx
+++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/AccountNetworkInfoExpanded.tsx
@@ -9,6 +9,7 @@ import useAccountInfo from '../../../../hooks/useAccountInfo';
import useApprovalRequest from '../../../../hooks/useApprovalRequest';
import InfoSection from '../../../UI/InfoRow/InfoSection';
import InfoRow from '../../../UI/InfoRow';
+import Address from '../../../UI/InfoRow/InfoValue/Address';
import InfoURL from '../../../UI/InfoRow/InfoValue/InfoURL';
// todo: use value component for address, network, currency value
@@ -23,7 +24,9 @@ const AccountNetworkInfoExpanded = () => {
return (
- {accountAddress}
+
+
+
{accountBalance}
diff --git a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap
index cae8ebe4e32..212fc75ed78 100644
--- a/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap
+++ b/app/components/Views/confirmations/components/Confirm/AccountNetworkInfo/AccountNetworkInfoExpanded/__snapshots__/AccountNetworkInfoExpanded.test.tsx.snap
@@ -49,19 +49,52 @@ exports[`AccountNetworkInfoExpanded should match snapshot for personal sign 1`]
Account
-
- 0x935E73EDb9fF52E23BaC7F7e043A1ecD06d05477
-
+
+
+ 0x935E7...05477
+
+
{
+ it('should match snapshot', async () => {
+ const container = renderWithProvider(, {
+ state: mockInitialState,
+ });
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/Address.tsx b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/Address.tsx
new file mode 100644
index 00000000000..2cb85054c7b
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/Address.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import Name from '../../../../../../../UI/Name';
+import { NameType } from '../../../../../../../UI/Name/Name.types';
+
+interface AddressProps {
+ address: string;
+}
+
+const Address = ({ address }: AddressProps) => (
+
+);
+
+export default Address;
diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/__snapshots__/Address.test.tsx.snap b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/__snapshots__/Address.test.tsx.snap
new file mode 100644
index 00000000000..c0a8749fcc1
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/__snapshots__/Address.test.tsx.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InfoAddress should match snapshot 1`] = `
+
+
+
+ 0xC4955...4D272
+
+
+`;
diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/index.ts b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/index.ts
new file mode 100644
index 00000000000..189acc8b2c5
--- /dev/null
+++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/Address/index.ts
@@ -0,0 +1 @@
+export { default } from './Address';
diff --git a/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoURL/InfoURL.test.tsx b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoURL/InfoURL.test.tsx
index 1522f6dcd23..88bcc940b85 100644
--- a/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoURL/InfoURL.test.tsx
+++ b/app/components/Views/confirmations/components/UI/InfoRow/InfoValue/InfoURL/InfoURL.test.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react-native';
-import InfoURL from './index';
+import InfoURL from './InfoURL';
describe('InfoURL', () => {
it('should display url as expected', async () => {
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 0becb6358b7..6bccce37031 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -144,6 +144,7 @@ const Routes = {
},
STAKING: {
STAKE: 'Stake',
+ STAKE_CONFIRMATION: 'StakeConfirmation',
UNSTAKE: 'Unstake',
CLAIM: 'Claim',
MODALS: {
diff --git a/app/core/AppConstants.ts b/app/core/AppConstants.ts
index 4999b670957..8aea9a7cb72 100644
--- a/app/core/AppConstants.ts
+++ b/app/core/AppConstants.ts
@@ -129,6 +129,7 @@ export default {
'https://support.metamask.io/privacy-and-security/privacy-best-practices',
SMART_TXS:
'https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/',
+ STAKING_RISK_DISCLOSURE: 'https://consensys.io/staking-risk-disclosures',
},
ERRORS: {
INFURA_BLOCKED_MESSAGE:
diff --git a/app/core/AppStateEventListener.test.ts b/app/core/AppStateEventListener.test.ts
index 3cf93c94aac..34f7c6ad07a 100644
--- a/app/core/AppStateEventListener.test.ts
+++ b/app/core/AppStateEventListener.test.ts
@@ -3,7 +3,7 @@ import { store } from '../store';
import Logger from '../util/Logger';
import { MetaMetrics, MetaMetricsEvents } from './Analytics';
import { AppStateEventListener } from './AppStateEventListener';
-import extractURLParams from './DeeplinkManager/ParseManager/extractURLParams';
+import { processAttribution } from './processAttribution';
jest.mock('react-native', () => ({
AppState: {
@@ -27,12 +27,14 @@ jest.mock('../store', () => ({
},
}));
-jest.mock('./DeeplinkManager/ParseManager/extractURLParams', () => jest.fn());
-
jest.mock('../util/Logger', () => ({
error: jest.fn(),
}));
+jest.mock('./processAttribution', () => ({
+ processAttribution: jest.fn(),
+}));
+
describe('AppStateEventListener', () => {
let appStateManager: AppStateEventListener;
let mockAppStateListener: (state: AppStateStatus) => void;
@@ -66,11 +68,15 @@ describe('AppStateEventListener', () => {
expect(Logger.error).toHaveBeenCalledWith(new Error('store is already initialized'));
});
- it('tracks event when app becomes active and conditions are met', () => {
- (store.getState as jest.Mock).mockReturnValue({
- security: { dataCollectionForMarketing: true },
- });
- (extractURLParams as jest.Mock).mockReturnValue({ params: { attributionId: 'test123' } });
+ it('tracks event when app becomes active and attribution data is available', () => {
+ const mockAttribution = {
+ attributionId: 'test123',
+ utm: 'test_utm',
+ utm_source: 'source',
+ utm_medium: 'medium',
+ utm_campaign: 'campaign',
+ };
+ (processAttribution as jest.Mock).mockReturnValue(mockAttribution);
appStateManager.setCurrentDeeplink('metamask://connect?attributionId=test123');
mockAppStateListener('active');
@@ -78,51 +84,31 @@ describe('AppStateEventListener', () => {
expect(mockTrackEvent).toHaveBeenCalledWith(
MetaMetricsEvents.APP_OPENED,
- { attributionId: 'test123' },
+ { attributionId: 'test123', utm_source: 'source', utm_medium: 'medium', utm_campaign: 'campaign' },
true
);
});
- it('does not track event when data collection is disabled', () => {
- (store.getState as jest.Mock).mockReturnValue({
- security: { dataCollectionForMarketing: false },
- });
+ it('does not track event when processAttribution returns undefined', () => {
+ (processAttribution as jest.Mock).mockReturnValue(undefined);
mockAppStateListener('active');
jest.advanceTimersByTime(2000);
- expect(mockTrackEvent).toHaveBeenCalledWith(
- MetaMetricsEvents.APP_OPENED,
- {},
- true
- );
- });
-
- it('does not track event when there is no deeplink', () => {
- (store.getState as jest.Mock).mockReturnValue({
- security: { dataCollectionForMarketing: true },
- });
-
- mockAppStateListener('active');
- jest.advanceTimersByTime(2000);
-
- expect(mockTrackEvent).toHaveBeenCalledWith(
- MetaMetricsEvents.APP_OPENED,
- { attributionId: undefined },
- true
- );
+ expect(mockTrackEvent).not.toHaveBeenCalled();
});
it('handles errors gracefully', () => {
- (store.getState as jest.Mock).mockImplementation(() => {
- throw new Error('Test error');
+ const testError = new Error('Test error');
+ (processAttribution as jest.Mock).mockImplementation(() => {
+ throw testError;
});
mockAppStateListener('active');
jest.advanceTimersByTime(2000);
expect(Logger.error).toHaveBeenCalledWith(
- expect.any(Error),
+ testError,
'AppStateManager: Error processing app state change'
);
expect(mockTrackEvent).not.toHaveBeenCalled();
diff --git a/app/core/AppStateEventListener.ts b/app/core/AppStateEventListener.ts
index 46752626e2a..8a9046462f6 100644
--- a/app/core/AppStateEventListener.ts
+++ b/app/core/AppStateEventListener.ts
@@ -51,13 +51,16 @@ export class AppStateEventListener {
}
try {
- const attributionId = processAttribution({ currentDeeplink: this.currentDeeplink, store: this.store });
- DevLogger.log(`AppStateManager:: processAppStateChange:: sending event 'APP_OPENED' attributionId=${attributionId}`);
- MetaMetrics.getInstance().trackEvent(
- MetaMetricsEvents.APP_OPENED,
- { attributionId },
- true
- );
+ const attribution = processAttribution({ currentDeeplink: this.currentDeeplink, store: this.store });
+ if(attribution) {
+ const { attributionId, utm, ...utmParams } = attribution;
+ DevLogger.log(`AppStateManager:: processAppStateChange:: sending event 'APP_OPENED' attributionId=${attribution.attributionId} utm=${attribution.utm}`, utmParams);
+ MetaMetrics.getInstance().trackEvent(
+ MetaMetricsEvents.APP_OPENED,
+ { attributionId, ...utmParams },
+ true
+ );
+ }
} catch (error) {
Logger.error(error as Error, 'AppStateManager: Error processing app state change');
}
diff --git a/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts b/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts
index 50b942a1366..423f78628a4 100644
--- a/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts
+++ b/app/core/DeeplinkManager/ParseManager/extractURLParams.test.ts
@@ -43,6 +43,7 @@ describe('extractURLParams', () => {
comm: 'test',
v: '2',
attributionId: '',
+ utm: '',
};
mockUrlParser.mockImplementation(
@@ -83,6 +84,7 @@ describe('extractURLParams', () => {
pubkey: '',
v: '',
attributionId: '',
+ utm: '',
});
});
@@ -116,6 +118,7 @@ describe('extractURLParams', () => {
pubkey: '',
v: '',
attributionId: '',
+ utm: '',
});
expect(alertSpy).toHaveBeenCalledWith(
@@ -137,6 +140,7 @@ describe('extractURLParams', () => {
sdkVersion: '',
pubkey: 'xyz',
attributionId: '',
+ utm: '',
};
mockUrlParser.mockImplementation(
diff --git a/app/core/DeeplinkManager/ParseManager/extractURLParams.ts b/app/core/DeeplinkManager/ParseManager/extractURLParams.ts
index 723ce7148b7..4d992b6c3e4 100644
--- a/app/core/DeeplinkManager/ParseManager/extractURLParams.ts
+++ b/app/core/DeeplinkManager/ParseManager/extractURLParams.ts
@@ -20,6 +20,7 @@ export interface DeeplinkUrlParams {
originatorInfo?: string;
request?: string;
attributionId?: string;
+ utm?: string;
account?: string; // This is the format => "address@chainId"
}
@@ -41,6 +42,7 @@ function extractURLParams(url: string) {
channelId: '',
comm: '',
attributionId: '',
+ utm: '',
};
DevLogger.log(`extractParams:: urlObj`, urlObj);
diff --git a/app/core/Engine.ts b/app/core/Engine.ts
index 48cc6279e84..95199c1b9eb 100644
--- a/app/core/Engine.ts
+++ b/app/core/Engine.ts
@@ -232,7 +232,7 @@ import { selectSwapsChainFeatureFlags } from '../reducers/swaps';
import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types';
import { submitSmartTransactionHook } from '../util/smart-transactions/smart-publish-hook';
import { zeroAddress } from 'ethereumjs-util';
-import { toChecksumHexAddress } from '@metamask/controller-utils';
+import { ApprovalType, toChecksumHexAddress } from '@metamask/controller-utils';
import { ExtendedControllerMessenger } from './ExtendedControllerMessenger';
import EthQuery from '@metamask/eth-query';
import DomainProxyMap from '../lib/DomainProxyMap/DomainProxyMap';
@@ -514,11 +514,8 @@ class Engine {
}),
showApprovalRequest: () => undefined,
typesExcludedFromRateLimiting: [
- // TODO: Replace with ApprovalType enum from @metamask/controller-utils when breaking change is fixed
- 'personal_sign',
- 'eth_signTypedData',
- 'transaction',
- 'wallet_watchAsset',
+ ApprovalType.Transaction,
+ ApprovalType.WatchAsset
],
});
@@ -569,7 +566,6 @@ class Engine {
chainId: networkController.getNetworkClientById(
networkController?.state.selectedNetworkClientId,
).configuration.chainId,
- // @ts-expect-error TODO: Resolve bump the assets controller version.
getNetworkClientById:
networkController.getNetworkClientById.bind(networkController),
});
@@ -654,7 +650,6 @@ class Engine {
networkController?.state.selectedNetworkClientId,
).configuration.chainId,
selectedAddress: preferencesController.state.selectedAddress,
- // @ts-expect-error TODO: Resolve provider type mismatch
provider: networkController.getProviderAndBlockTracker().provider,
state: initialState.TokensController,
// @ts-expect-error TODO: Resolve mismatch between base-controller versions.
@@ -953,7 +948,6 @@ class Engine {
networkController?.state.selectedNetworkClientId,
).configuration.chainId,
),
- // @ts-expect-error TODO: Resolve mismatch between base-controller versions.
getNetworkClientById:
networkController.getNetworkClientById.bind(networkController),
});
@@ -1529,7 +1523,6 @@ class Engine {
selectedAddress: preferencesController.state.selectedAddress,
tokenPricesService: codefiTokenApiV2,
interval: 30 * 60 * 1000,
- // @ts-expect-error TODO: Resolve mismatch between base-controller versions.
getNetworkClientById:
networkController.getNetworkClientById.bind(networkController),
}),
@@ -1780,7 +1773,6 @@ class Engine {
}
provider.sendAsync = provider.sendAsync.bind(provider);
AccountTrackerController.configure({ provider });
- // @ts-expect-error TODO: Resolve mismatch between base-controller versions.
AssetsContractController.configure({ provider });
SwapsController.configure({
diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts
index c46e5118ff5..0c24c4821a1 100644
--- a/app/core/EngineService/EngineService.ts
+++ b/app/core/EngineService/EngineService.ts
@@ -78,7 +78,7 @@ class EngineService {
},
{
name: 'PhishingController',
- key: `${engine.context.PhishingController.name}:maybeUpdateState`,
+ key: `${engine.context.PhishingController.name}:stateChange`,
},
{
name: 'PreferencesController',
diff --git a/app/core/processAttribution.test.tsx b/app/core/processAttribution.test.tsx
index fa4a196a2e2..79c29c2b908 100644
--- a/app/core/processAttribution.test.tsx
+++ b/app/core/processAttribution.test.tsx
@@ -1,6 +1,7 @@
import { store } from '../store';
import extractURLParams from './DeeplinkManager/ParseManager/extractURLParams';
import { processAttribution } from './processAttribution';
+import Logger from '../util/Logger';
jest.mock('../store', () => ({
store: {
@@ -9,22 +10,42 @@ jest.mock('../store', () => ({
}));
jest.mock('./DeeplinkManager/ParseManager/extractURLParams', () => jest.fn());
+jest.mock('../util/Logger', () => ({
+ error: jest.fn(),
+}));
describe('processAttribution', () => {
beforeEach(() => {
jest.clearAllMocks();
});
- it('returns attributionId when marketing is enabled and deeplink is provided', () => {
+ it('returns attribution data when marketing is enabled and deeplink is provided', () => {
(store.getState as jest.Mock).mockReturnValue({
security: { dataCollectionForMarketing: true },
});
(extractURLParams as jest.Mock).mockReturnValue({
- params: { attributionId: 'test123' },
+ params: {
+ attributionId: 'test123',
+ utm: JSON.stringify({
+ source: 'twitter',
+ medium: 'social',
+ campaign: 'cmp-57731027-afbf09/',
+ term: null,
+ content: null
+ })
+ },
});
- const result = processAttribution({ currentDeeplink: 'metamask://connect?attributionId=test123', store });
- expect(result).toBe('test123');
+ const result = processAttribution({ currentDeeplink: 'metamask://connect?attributionId=test123&utm=...', store });
+ expect(result).toEqual({
+ attributionId: 'test123',
+ utm: expect.any(String),
+ utm_source: 'twitter',
+ utm_medium: 'social',
+ utm_campaign: 'cmp-57731027-afbf09/',
+ utm_term: null,
+ utm_content: null
+ });
});
it('returns undefined when marketing is disabled', () => {
@@ -32,7 +53,7 @@ describe('processAttribution', () => {
security: { dataCollectionForMarketing: false },
});
- const result = processAttribution({ currentDeeplink: 'metamask://connect?attributionId=test123', store });
+ const result = processAttribution({ currentDeeplink: 'metamask://connect?attributionId=test123&utm=...', store });
expect(result).toBeUndefined();
});
@@ -45,15 +66,53 @@ describe('processAttribution', () => {
expect(result).toBeUndefined();
});
- it('returns undefined when attributionId is not present in params', () => {
+ it('returns partial data when some UTM params are missing', () => {
(store.getState as jest.Mock).mockReturnValue({
security: { dataCollectionForMarketing: true },
});
(extractURLParams as jest.Mock).mockReturnValue({
- params: {},
+ params: {
+ attributionId: 'test123',
+ utm: JSON.stringify({
+ source: 'twitter',
+ medium: 'social'
+ })
+ },
});
- const result = processAttribution({ currentDeeplink: 'metamask://connect', store });
- expect(result).toBeUndefined();
+ const result = processAttribution({ currentDeeplink: 'metamask://connect?attributionId=test123&utm=...', store });
+ expect(result).toEqual({
+ attributionId: 'test123',
+ utm: expect.any(String),
+ utm_source: 'twitter',
+ utm_medium: 'social',
+ utm_campaign: undefined,
+ utm_term: undefined,
+ utm_content: undefined
+ });
+ });
+
+ it('handles JSON parsing errors gracefully', () => {
+ (store.getState as jest.Mock).mockReturnValue({
+ security: { dataCollectionForMarketing: true },
+ });
+ (extractURLParams as jest.Mock).mockReturnValue({
+ params: {
+ attributionId: 'test123',
+ utm: 'invalid-json'
+ },
+ });
+
+ const result = processAttribution({ currentDeeplink: 'metamask://connect?attributionId=test123&utm=invalid-json', store });
+ expect(result).toEqual({
+ attributionId: 'test123',
+ utm: 'invalid-json',
+ utm_source: undefined,
+ utm_medium: undefined,
+ utm_campaign: undefined,
+ utm_term: undefined,
+ utm_content: undefined
+ });
+ expect(Logger.error).toHaveBeenCalledWith(expect.any(Error), expect.any(Error));
});
});
diff --git a/app/core/processAttribution.tsx b/app/core/processAttribution.tsx
index f3a604e3cf5..d49518bdaaa 100644
--- a/app/core/processAttribution.tsx
+++ b/app/core/processAttribution.tsx
@@ -1,6 +1,8 @@
import extractURLParams from './DeeplinkManager/ParseManager/extractURLParams';
import { RootState } from '../reducers';
import { Store } from 'redux';
+import Logger from '../util/Logger';
+import DevLogger from './SDKConnect/utils/DevLogger';
interface ProcessAttributionParams {
currentDeeplink: string | null;
@@ -8,13 +10,51 @@ interface ProcessAttributionParams {
store: Store;
}
-export function processAttribution({ currentDeeplink, store }: ProcessAttributionParams): string | undefined {
- const state = store.getState();
- const isMarketingEnabled = state.security.dataCollectionForMarketing;
+interface AttributionResult {
+ attributionId?: string;
+ utm?: string;
+ utm_source?: string;
+ utm_medium?: string;
+ utm_campaign?: string;
+ utm_term?: string;
+ utm_content?: string;
+}
+
+export function processAttribution({ currentDeeplink, store }: ProcessAttributionParams): AttributionResult | undefined {
+ const { security } = store.getState();
+ if (!security.dataCollectionForMarketing) {
+ return undefined;
+ }
- if (isMarketingEnabled && currentDeeplink) {
+ if (currentDeeplink) {
const { params } = extractURLParams(currentDeeplink);
- return params.attributionId || undefined; // Force undefined to be returned as extractUrlParams default to empty string on error.
+ const attributionId = params.attributionId || undefined;
+ const utm = params.utm || undefined;
+ let utm_source, utm_medium, utm_campaign, utm_term, utm_content;
+
+ if (utm) {
+ try {
+ const utmParams = JSON.parse(utm);
+ DevLogger.log('processAttribution:: UTM params', utmParams);
+ utm_source = utmParams.source;
+ utm_medium = utmParams.medium;
+ utm_campaign = utmParams.campaign;
+ utm_term = utmParams.term;
+ utm_content = utmParams.content;
+ } catch (error) {
+ Logger.error(new Error('Error parsing UTM params'), error);
+ }
+ }
+
+ return {
+ attributionId,
+ utm,
+ utm_source,
+ utm_medium,
+ utm_campaign,
+ utm_term,
+ utm_content
+ };
}
return undefined;
diff --git a/app/images/flare-mainnet.png b/app/images/flare-mainnet.png
new file mode 100644
index 00000000000..07dac76783f
Binary files /dev/null and b/app/images/flare-mainnet.png differ
diff --git a/app/images/songbird.png b/app/images/songbird.png
new file mode 100644
index 00000000000..2af76400504
Binary files /dev/null and b/app/images/songbird.png differ
diff --git a/app/selectors/nftController.ts b/app/selectors/nftController.ts
index a2a3a669074..b6a217f727a 100644
--- a/app/selectors/nftController.ts
+++ b/app/selectors/nftController.ts
@@ -1,5 +1,5 @@
import { createSelector } from 'reselect';
-import { NftControllerState } from '@metamask/assets-controllers';
+import { Nft, NftControllerState } from '@metamask/assets-controllers';
import { RootState } from '../reducers';
const selectNftControllerState = (state: RootState) =>
@@ -15,3 +15,14 @@ export const selectAllNfts = createSelector(
selectNftControllerState,
(nftControllerState: NftControllerState) => nftControllerState.allNfts,
);
+
+export const selectAllNftsFlat = createSelector(
+ selectAllNfts,
+ (nftsByChainByAccount) => {
+ const nftsByChainArray = Object.values(nftsByChainByAccount);
+ return nftsByChainArray.reduce((acc, nftsByChain) => {
+ const nftsArrays = Object.values(nftsByChain);
+ return acc.concat(...nftsArrays);
+ }, [] as Nft[]);
+ },
+);
diff --git a/app/selectors/tokensController.ts b/app/selectors/tokensController.ts
index f63f4c857a6..ccebe64ebfb 100644
--- a/app/selectors/tokensController.ts
+++ b/app/selectors/tokensController.ts
@@ -37,3 +37,24 @@ export const selectDetectedTokens = createSelector(
(tokensControllerState: TokensControllerState) =>
tokensControllerState?.detectedTokens,
);
+
+const selectAllTokens = createSelector(
+ selectTokensControllerState,
+ (tokensControllerState: TokensControllerState) =>
+ tokensControllerState?.allTokens,
+);
+
+export const selectAllTokensFlat = createSelector(
+ selectAllTokens,
+ (tokensByAccountByChain) => {
+ if (Object.values(tokensByAccountByChain).length === 0) {
+ return [];
+ }
+ const tokensByAccountArray = Object.values(tokensByAccountByChain);
+
+ return tokensByAccountArray.reduce((acc, tokensByAccount) => {
+ const tokensArray = Object.values(tokensByAccount);
+ return acc.concat(...tokensArray);
+ }, [] as Token[]);
+ },
+);
diff --git a/app/store/index.ts b/app/store/index.ts
index aa7a4df512f..9cc858e9ac0 100644
--- a/app/store/index.ts
+++ b/app/store/index.ts
@@ -9,10 +9,14 @@ import { Authentication } from '../core';
import LockManagerService from '../core/LockManagerService';
import ReadOnlyNetworkStore from '../util/test/network-store';
import { isE2E } from '../util/test/utils';
+import { trace, endTrace, TraceName, TraceOperation } from '../util/trace';
+import StorageWrapper from './storage-wrapper';
+
import thunk from 'redux-thunk';
import persistConfig from './persistConfig';
import { AppStateEventProcessor } from '../core/AppStateEventListener';
+import { getTraceTags } from '../util/sentry/tags';
// TODO: Improve type safety by using real Action types instead of `any`
// TODO: Replace "any" with type
@@ -24,7 +28,7 @@ const pReducer = persistReducer(persistConfig, rootReducer);
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any, import/no-mutable-exports
let store: Store, persistor;
-const createStoreAndPersistor = async () => {
+const createStoreAndPersistor = async (appStartTime: number) => {
// Obtain the initial state from ReadOnlyNetworkStore for E2E tests.
const initialState = isE2E
? await ReadOnlyNetworkStore.getState()
@@ -46,6 +50,24 @@ const createStoreAndPersistor = async () => {
middlewares.push(createReduxFlipperDebugger());
}
+ const jsStartTime = performance.now();
+
+ trace({
+ name: TraceName.LoadScripts,
+ op: TraceOperation.LoadScripts,
+ startTime: appStartTime,
+ });
+
+ endTrace({
+ name: TraceName.LoadScripts,
+ timestamp: appStartTime + jsStartTime,
+ });
+
+ trace({
+ name: TraceName.CreateStore,
+ op: TraceOperation.CreateStore,
+ });
+
store = configureStore({
reducer: pReducer,
middleware: middlewares,
@@ -54,10 +76,19 @@ const createStoreAndPersistor = async () => {
sagaMiddleware.run(rootSaga);
+ endTrace({ name: TraceName.CreateStore });
+
+ trace({
+ name: TraceName.StorageRehydration,
+ op: TraceOperation.StorageRehydration,
+ });
+
/**
* Initialize services after persist is completed
*/
- const onPersistComplete = () => {
+ const onPersistComplete = async () => {
+ endTrace({ name: TraceName.StorageRehydration });
+
/**
* EngineService.initalizeEngine(store) with SES/lockdown:
* Requires ethjs nested patches (lib->src)
@@ -73,6 +104,7 @@ const createStoreAndPersistor = async () => {
* - TypeError: undefined is not an object (evaluating 'TokenListController.tokenList')
* - V8: SES_UNHANDLED_REJECTION
*/
+
store.dispatch({
type: 'TOGGLE_BASIC_FUNCTIONALITY',
basicFunctionalityEnabled:
@@ -83,7 +115,18 @@ const createStoreAndPersistor = async () => {
store.dispatch({
type: 'FETCH_FEATURE_FLAGS',
});
- EngineService.initalizeEngine(store);
+
+ await trace(
+ {
+ name: TraceName.EngineInitialization,
+ op: TraceOperation.EngineInitialization,
+ tags: getTraceTags(store.getState?.()),
+ },
+ () => {
+ EngineService.initalizeEngine(store);
+ },
+ );
+
Authentication.init(store);
AppStateEventProcessor.init(store);
LockManagerService.init(store);
@@ -93,7 +136,16 @@ const createStoreAndPersistor = async () => {
};
(async () => {
- await createStoreAndPersistor();
+ const appStartTime = await StorageWrapper.getItem('appStartTime');
+
+ await trace(
+ {
+ name: TraceName.UIStartup,
+ op: TraceOperation.UIStartup,
+ startTime: appStartTime,
+ },
+ async () => await createStoreAndPersistor(appStartTime),
+ );
})();
export { store, persistor };
diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts
index 2cd7477a432..bab5f5d74fe 100644
--- a/app/store/persistConfig.ts
+++ b/app/store/persistConfig.ts
@@ -68,14 +68,9 @@ const persistTransform = createTransform(
return inboundState;
}
- const {
- TokenListController,
- SwapsController,
- PhishingController,
- ...controllers
- } = inboundState.backgroundState || {};
- const { tokenList, tokensChainsCache, ...persistedTokenListController } =
- TokenListController;
+ const { SwapsController, ...controllers } =
+ inboundState.backgroundState || {};
+
const {
aggregatorMetadata,
aggregatorMetadataLastFetched,
@@ -87,16 +82,11 @@ const persistTransform = createTransform(
...persistedSwapsController
} = SwapsController;
- const { phishingLists, whitelist, ...persistedPhishingController } =
- PhishingController;
-
// Reconstruct data to persist
const newState = {
backgroundState: {
...controllers,
- TokenListController: persistedTokenListController,
SwapsController: persistedSwapsController,
- PhishingController: persistedPhishingController,
},
};
return newState;
diff --git a/app/util/networks/customNetworks.tsx b/app/util/networks/customNetworks.tsx
index fc98b3eb65c..8f1518733cd 100644
--- a/app/util/networks/customNetworks.tsx
+++ b/app/util/networks/customNetworks.tsx
@@ -129,3 +129,8 @@ export const UnpopularNetworkList = [
},
},
];
+
+export const CustomNetworkImgMapping: Record<`0x${string}`, string> = {
+ '0xe': require('../../images/flare-mainnet.png'), // Flare Mainnet
+ '0x13': require('../../images/songbird.png'), // Songbird Testnet
+};
diff --git a/app/util/networks/index.js b/app/util/networks/index.js
index e942a4b7512..6a91fa89a9b 100644
--- a/app/util/networks/index.js
+++ b/app/util/networks/index.js
@@ -32,7 +32,11 @@ const lineaTestnetLogo = require('../../images/linea-testnet-logo.png');
const lineaMainnetLogo = require('../../images/linea-mainnet-logo.png');
/* eslint-enable */
-import { PopularList, UnpopularNetworkList } from './customNetworks';
+import {
+ PopularList,
+ UnpopularNetworkList,
+ CustomNetworkImgMapping,
+} from './customNetworks';
import { strings } from '../../../locales/i18n';
import {
getEtherscanAddressUrl,
@@ -433,6 +437,8 @@ export const getNetworkImageSource = ({ networkType, chainId }) => {
(networkConfig) => networkConfig.chainId === chainId,
);
+ const customNetworkImg = CustomNetworkImgMapping[chainId];
+
const popularNetwork = PopularList.find(
(networkConfig) => networkConfig.chainId === chainId,
);
@@ -441,6 +447,9 @@ export const getNetworkImageSource = ({ networkType, chainId }) => {
if (network) {
return network.rpcPrefs.imageSource;
}
+ if (customNetworkImg) {
+ return customNetworkImg;
+ }
return getTestNetImage(networkType);
};
diff --git a/app/util/notifications/androidChannels.test.ts b/app/util/notifications/androidChannels.test.ts
index c0cb6a45412..debc8448ab3 100644
--- a/app/util/notifications/androidChannels.test.ts
+++ b/app/util/notifications/androidChannels.test.ts
@@ -6,11 +6,11 @@ import {
} from './androidChannels';
describe('notificationChannels', () => {
- it('should have two channels', () => {
+ it('contains two channels', () => {
expect(notificationChannels).toHaveLength(2);
});
- it('should have the correct properties for the first channel', () => {
+ it('first channel has DEFAULT_NOTIFICATION_CHANNEL_ID', () => {
const firstChannel: MetaMaskAndroidChannel = notificationChannels[0];
expect(firstChannel).toEqual({
id: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID,
@@ -23,7 +23,7 @@ describe('notificationChannels', () => {
});
});
- it('should have the correct properties for the second channel', () => {
+ it('second channel should have the correct properties for DEFAULT_NOTIFICATION_CHANNEL_ID', () => {
const secondChannel: MetaMaskAndroidChannel = notificationChannels[1];
expect(secondChannel).toEqual({
id: ChannelId.ANNOUNCEMENT_NOTIFICATION_CHANNEL_ID,
@@ -36,13 +36,13 @@ describe('notificationChannels', () => {
});
});
- it('should have unique titles for each channel', () => {
+ it('channels have unique titles', () => {
const titles = notificationChannels.map((channel) => channel.title);
const uniqueTitles = new Set(titles);
expect(uniqueTitles.size).toBe(titles.length);
});
- it('should have unique subtitles for each channel', () => {
+ it('channels have unique subtitles ', () => {
const subtitles = notificationChannels.map((channel) => channel.subtitle);
const uniqueSubtitles = new Set(subtitles);
expect(uniqueSubtitles.size).toBe(subtitles.length);
diff --git a/app/util/notifications/hooks/index.test.ts b/app/util/notifications/hooks/index.test.ts
index bbe94a97dc3..ff4b9fd77aa 100644
--- a/app/util/notifications/hooks/index.test.ts
+++ b/app/util/notifications/hooks/index.test.ts
@@ -1,19 +1,13 @@
-import { renderHook, act } from '@testing-library/react-hooks';
-import notifee, {
- EventType,
- Event as NotifeeEvent,
-} from '@notifee/react-native';
-
+import { act, renderHook } from '@testing-library/react-hooks';
+// eslint-disable-next-line import/no-namespace
+import * as constants from '../constants';
import useNotificationHandler from './index';
import { NavigationProp, ParamListBase } from '@react-navigation/native';
import Routes from '../../../constants/navigation/Routes';
import { Notification } from '../../../util/notifications/types';
import { TRIGGER_TYPES } from '../constants';
+import NotificationsService from '../services/NotificationService';
-jest.mock('../../../util/device');
-jest.mock('../../../core/NotificationManager', () => ({
- setTransactionToView: jest.fn(),
-}));
jest.mock('@notifee/react-native', () => ({
setBadgeCount: jest.fn(),
decrementBadgeCount: jest.fn(),
@@ -29,12 +23,9 @@ jest.mock('@notifee/react-native', () => ({
},
}));
-jest.mock('../../../core/NotificationManager', () => ({
- setTransactionToView: jest.fn(),
-}));
-
-jest.mock('../../../util/device', () => ({
- isAndroid: jest.fn(),
+jest.mock('../constants', () => ({
+ ...jest.requireActual('../constants'),
+ isNotificationsFeatureEnabled: jest.fn(),
}));
const mockNavigate = jest.fn();
@@ -43,10 +34,10 @@ const mockNavigation = {
} as unknown as NavigationProp;
const notification = {
- id: 1,
+ id: '123',
type: TRIGGER_TYPES.ERC1155_RECEIVED,
data: {
- id: 1,
+ id: '123',
trigger_id: '1',
chain_id: 1,
block_number: 1,
@@ -62,22 +53,22 @@ const notification = {
},
} as unknown as Notification;
-const mockNotificationEvent = (event: NotifeeEvent) => ({
- type: event.type,
- detail: {
- notification,
- },
-});
-
+jest.mock('../services/NotificationService', () => ({
+ onForegroundEvent: jest.fn(),
+ onBackgroundEvent: jest.fn(),
+ handleNotificationEvent: jest.fn(),
+}));
describe('useNotificationHandler', () => {
beforeEach(() => {
jest.clearAllMocks();
});
- it('should navigate to NOTIFICATIONS.DETAILS if notification is pressed', async () => {
+ it('navigates to NOTIFICATIONS.DETAILS when notification is pressed', async () => {
const { result } = renderHook(() => useNotificationHandler(mockNavigation));
- await result.current.handlePressedNotification(notification);
+ await act(async () => {
+ result.current.handlePressedNotification(notification);
+ });
expect(mockNavigation.navigate).toHaveBeenCalledWith(
Routes.NOTIFICATIONS.DETAILS,
@@ -87,163 +78,32 @@ describe('useNotificationHandler', () => {
);
});
- it('should handle notifications correctly', async () => {
- const { waitFor } = renderHook(() =>
- useNotificationHandler(mockNavigation),
- );
+ it('does not navigates when notification is null', async () => {
- await act(async () => {
- notifee.onForegroundEvent(() =>
- mockNotificationEvent({
- type: EventType.PRESS,
- detail: {
- notification: {
- body: 'notificationTest',
- data: {
- action: 'tx',
- id: '123',
- },
- },
- },
- }),
- );
- await waitFor(() => {
- expect(notifee.onForegroundEvent).toHaveBeenCalled();
- });
- });
- });
- it('should do nothing if the EventType is DISMISSED', async () => {
- const { waitFor } = renderHook(() =>
+ const { result } = renderHook(() =>
useNotificationHandler(mockNavigation),
);
await act(async () => {
- notifee.onForegroundEvent(() =>
- mockNotificationEvent({
- type: EventType.DISMISSED,
- detail: {
- notification: {
- body: 'notificationTest',
- data: {
- action: 'tx',
- id: '123',
- },
- },
- },
- }),
- );
-
- await waitFor(() => {
- expect(notifee.onForegroundEvent).toHaveBeenCalled();
- });
+ result.current.handlePressedNotification();
});
- });
-
- it('should do nothing if data.action is not tx', async () => {
- const { waitFor } = renderHook(() =>
- useNotificationHandler(mockNavigation),
- );
- await act(async () => {
- notifee.onForegroundEvent(() =>
- mockNotificationEvent({
- type: EventType.DELIVERED,
- detail: {
- notification: {
- body: 'notificationTest',
- data: {
- action: 'no-tx',
- id: '123',
- },
- },
- },
- }),
- );
-
- await waitFor(() => {
- expect(notifee.onForegroundEvent).toHaveBeenCalled();
- });
-
- expect(mockNavigate).not.toHaveBeenCalled();
- });
+ expect(mockNavigation.navigate).not.toHaveBeenCalled();
});
- it('handleOpenedNotification should do nothing if notification is null', async () => {
- const { waitFor } = renderHook(() =>
- useNotificationHandler(mockNavigation),
- );
-
- await act(async () => {
- notifee.onForegroundEvent(() =>
- mockNotificationEvent({
- type: EventType.DELIVERED,
- detail: {
- notification: undefined,
- },
- }),
- );
- await waitFor(() => {
- expect(notifee.onForegroundEvent).toHaveBeenCalled();
- });
-
- expect(mockNavigate).not.toHaveBeenCalled();
- });
- });
+ it('does nothing if the isNotificationsFeatureEnabled is false', async () => {
+ jest.spyOn(constants, 'isNotificationsFeatureEnabled').mockReturnValue(false);
- it('should navigate to the transaction view when the notification action is "tx"', async () => {
- const { waitFor } = renderHook(() =>
- useNotificationHandler(mockNavigation),
- );
+ const { result } = renderHook(() => useNotificationHandler(mockNavigation));
await act(async () => {
- notifee.onForegroundEvent(() =>
- mockNotificationEvent({
- type: EventType.DELIVERED,
- detail: {
- notification: {
- body: 'notificationTest',
- data: {
- action: 'tx',
- id: '123',
- },
- },
- },
- }),
- );
- await waitFor(() => {
- expect(notifee.onForegroundEvent).toHaveBeenCalled();
- });
+ result.current.handlePressedNotification(notification);
});
- }, 10000);
- it('should process notification on Android', async () => {
- jest.doMock('react-native/Libraries/Utilities/Platform', () => ({
- OS: 'android',
- }));
+ expect(NotificationsService.onForegroundEvent).not.toHaveBeenCalled();
+ expect(NotificationsService.onBackgroundEvent).not.toHaveBeenCalled();
- const { waitFor } = renderHook(() =>
- useNotificationHandler(mockNavigation),
- );
-
- await act(async () => {
- notifee.onForegroundEvent(() =>
- mockNotificationEvent({
- type: EventType.PRESS,
- detail: {
- notification: {
- body: 'notificationTest',
- data: {
- action: 'tx',
- id: '123',
- },
- },
- },
- }),
- );
- await waitFor(() => {
- expect(notifee.onForegroundEvent).toHaveBeenCalled();
- });
- });
+ jest.restoreAllMocks();
});
});
diff --git a/app/util/notifications/hooks/index.ts b/app/util/notifications/hooks/index.ts
index e91fe593eaf..3f6bb98c9a3 100644
--- a/app/util/notifications/hooks/index.ts
+++ b/app/util/notifications/hooks/index.ts
@@ -4,9 +4,11 @@ import NotificationsService from '../../../util/notifications/services/Notificat
import Routes from '../../../constants/navigation/Routes';
import {
isNotificationsFeatureEnabled,
- TRIGGER_TYPES,
} from '../../../util/notifications';
import { Notification } from '../../../util/notifications/types';
+import {
+ TRIGGER_TYPES,
+} from '../../../util/notifications/constants';
import { Linking } from 'react-native';
const useNotificationHandler = (navigation: NavigationProp) => {
diff --git a/app/util/sentry/tags/index.test.ts b/app/util/sentry/tags/index.test.ts
new file mode 100644
index 00000000000..5511ab7cf92
--- /dev/null
+++ b/app/util/sentry/tags/index.test.ts
@@ -0,0 +1,229 @@
+import { RootState } from '../../../reducers';
+import { getTraceTags } from './';
+import initialRootState, {
+ backgroundState,
+} from '../../../util/test/initial-root-state';
+import { userInitialState } from '../../../reducers/user';
+import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils';
+
+describe('Tags Utils', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('getTraceTags', () => {
+ it('includes if unlocked', () => {
+ const state = {
+ ...initialRootState,
+ user: { ...userInitialState, userLoggedIn: true },
+ };
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.unlocked']).toStrictEqual(true);
+ });
+
+ it('includes if not unlocked', () => {
+ const state = {
+ ...initialRootState,
+ user: { ...userInitialState, userLoggedIn: false },
+ };
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.unlocked']).toStrictEqual(false);
+ });
+
+ it('includes pending approval type', () => {
+ const state = {
+ ...initialRootState,
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ ApprovalController: {
+ ...backgroundState.ApprovalController,
+ pendingApprovals: {
+ 1: {
+ type: 'eth_sendTransaction',
+ },
+ },
+ },
+ },
+ },
+ } as unknown as RootState;
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.pending_approval']).toStrictEqual(
+ 'eth_sendTransaction',
+ );
+ });
+
+ it('includes first pending approval type if multiple', () => {
+ const state = {
+ ...initialRootState,
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+
+ ApprovalController: {
+ ...backgroundState.ApprovalController,
+ pendingApprovals: {
+ 1: {
+ type: 'eth_sendTransaction',
+ },
+ 2: {
+ type: 'personal_sign',
+ },
+ },
+ },
+ },
+ },
+ } as unknown as RootState;
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.pending_approval']).toStrictEqual(
+ 'eth_sendTransaction',
+ );
+ });
+
+ it('includes account count', () => {
+ const state = {
+ ...initialRootState,
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ AccountsController: createMockAccountsControllerState([
+ '0x1234',
+ '0x4321',
+ ]),
+ },
+ },
+ } as unknown as RootState;
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.account_count']).toStrictEqual(2);
+ });
+
+ it('includes nft count', () => {
+ const state = {
+ ...initialRootState,
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ NftController: {
+ ...backgroundState.NftController,
+ allNfts: {
+ '0x1234': {
+ '0x1': [
+ {
+ tokenId: '1',
+ },
+ {
+ tokenId: '2',
+ },
+ ],
+ '0x2': [
+ {
+ tokenId: '3',
+ },
+ {
+ tokenId: '4',
+ },
+ ],
+ },
+ '0x4321': {
+ '0x3': [
+ {
+ tokenId: '5',
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ } as unknown as RootState;
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.nft_count']).toStrictEqual(5);
+ });
+
+ it('includes notification count', () => {
+ const state = {
+ ...initialRootState,
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ NotificationServicesController: {
+ metamaskNotificationsList: [{}, {}, {}],
+ },
+ },
+ },
+ } as unknown as RootState;
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.notification_count']).toStrictEqual(3);
+ });
+
+ it('includes token count', () => {
+ const state = {
+ ...initialRootState,
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ TokensController: {
+ allTokens: {
+ '0x1': {
+ '0x1234': [{}, {}],
+ '0x4321': [{}],
+ },
+ '0x2': {
+ '0x5678': [{}],
+ },
+ },
+ },
+ },
+ },
+ } as unknown as RootState;
+
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.token_count']).toStrictEqual(4);
+ });
+
+ it('includes transaction count', () => {
+ const state = {
+ ...initialRootState,
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ TransactionController: {
+ transactions: [
+ {
+ id: 1,
+ chainId: '0x1',
+ },
+ {
+ id: 2,
+ chainId: '0x1',
+ },
+ {
+ id: 3,
+ chainId: '0x2',
+ },
+ ],
+ },
+ },
+ },
+ } as unknown as RootState;
+ const tags = getTraceTags(state);
+
+ expect(tags?.['wallet.transaction_count']).toStrictEqual(3);
+ });
+ });
+});
diff --git a/app/util/sentry/tags/index.ts b/app/util/sentry/tags/index.ts
new file mode 100644
index 00000000000..796bb9212fe
--- /dev/null
+++ b/app/util/sentry/tags/index.ts
@@ -0,0 +1,30 @@
+import { RootState } from '../../../reducers';
+import { selectAllNftsFlat } from '../../../selectors/nftController';
+import { selectInternalAccounts } from '../../../selectors/accountsController';
+import { selectAllTokensFlat } from '../../../selectors/tokensController';
+import { getNotificationsList } from '../../../selectors/notifications';
+import { selectTransactions } from '../../../selectors/transactionController';
+import { selectPendingApprovals } from '../../../selectors/approvalController';
+
+export function getTraceTags(state: RootState) {
+ if (!Object.keys(state?.engine?.backgroundState).length) return;
+ const unlocked = state.user.userLoggedIn;
+ const accountCount = selectInternalAccounts(state).length;
+ const nftCount = selectAllNftsFlat(state).length;
+ const notificationCount = getNotificationsList(state).length;
+ const tokenCount = selectAllTokensFlat(state).length;
+ const transactionCount = selectTransactions(state).length;
+ const pendingApprovals = Object.values(selectPendingApprovals(state));
+
+ const firstApprovalType = pendingApprovals?.[0]?.type;
+
+ return {
+ 'wallet.account_count': accountCount,
+ 'wallet.nft_count': nftCount,
+ 'wallet.notification_count': notificationCount,
+ 'wallet.pending_approval': firstApprovalType,
+ 'wallet.token_count': tokenCount,
+ 'wallet.transaction_count': transactionCount,
+ 'wallet.unlocked': unlocked,
+ };
+}
diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js
index 26448b31041..024422616a6 100644
--- a/app/util/sentry/utils.js
+++ b/app/util/sentry/utils.js
@@ -503,7 +503,7 @@ export function setupSentry() {
]
: integrations,
// Set tracesSampleRate to 1.0, as that ensures that every transaction will be sent to Sentry for development builds.
- tracesSampleRate: __DEV__ ? 1.0 : 0.08,
+ tracesSampleRate: __DEV__ ? 1.0 : 0.04,
beforeSend: (report) => rewriteReport(report),
beforeBreadcrumb: (breadcrumb) => rewriteBreadcrumb(breadcrumb),
beforeSendTransaction: (event) => excludeEvents(event),
diff --git a/app/util/trace.test.ts b/app/util/trace.test.ts
index cb90722c9cb..b9e541ebdc5 100644
--- a/app/util/trace.test.ts
+++ b/app/util/trace.test.ts
@@ -1,17 +1,19 @@
-import { startSpan, startSpanManual, withScope } from '@sentry/react-native';
+import {
+ Scope,
+ setMeasurement,
+ startSpan,
+ startSpanManual,
+ withScope,
+} from '@sentry/react-native';
import { Span } from '@sentry/types';
-import {
- endTrace,
- trace,
- TraceName,
- TRACES_CLEANUP_INTERVAL,
-} from './trace';
+import { endTrace, trace, TraceName, TRACES_CLEANUP_INTERVAL } from './trace';
jest.mock('@sentry/react-native', () => ({
withScope: jest.fn(),
startSpan: jest.fn(),
startSpanManual: jest.fn(),
+ setMeasurement: jest.fn(),
}));
const NAME_MOCK = TraceName.Middleware;
@@ -36,15 +38,23 @@ describe('Trace', () => {
const startSpanMock = jest.mocked(startSpan);
const startSpanManualMock = jest.mocked(startSpanManual);
const withScopeMock = jest.mocked(withScope);
- const setTagsMock = jest.fn();
+ const setMeasurementMock = jest.mocked(setMeasurement);
+ const setTagMock = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
startSpanMock.mockImplementation((_, fn) => fn({} as Span));
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- withScopeMock.mockImplementation((fn: any) => fn({ setTags: setTagsMock }));
+ startSpanManualMock.mockImplementation((_, fn) =>
+ fn({} as Span, () => {
+ // Intentionally empty
+ }),
+ );
+
+ withScopeMock.mockImplementation((fn: (arg: Scope) => unknown) =>
+ fn({ setTag: setTagMock } as unknown as Scope),
+ );
});
describe('trace', () => {
@@ -87,8 +97,12 @@ describe('Trace', () => {
expect.any(Function),
);
- expect(setTagsMock).toHaveBeenCalledTimes(1);
- expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK);
+ expect(setTagMock).toHaveBeenCalledTimes(2);
+ expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1');
+ expect(setTagMock).toHaveBeenCalledWith('tag2', true);
+
+ expect(setMeasurementMock).toHaveBeenCalledTimes(1);
+ expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none');
});
it('invokes Sentry if no callback provided', () => {
@@ -113,8 +127,12 @@ describe('Trace', () => {
expect.any(Function),
);
- expect(setTagsMock).toHaveBeenCalledTimes(1);
- expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK);
+ expect(setTagMock).toHaveBeenCalledTimes(2);
+ expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1');
+ expect(setTagMock).toHaveBeenCalledWith('tag2', true);
+
+ expect(setMeasurementMock).toHaveBeenCalledTimes(1);
+ expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none');
});
it('invokes Sentry if no callback provided with custom start time', () => {
@@ -141,8 +159,12 @@ describe('Trace', () => {
expect.any(Function),
);
- expect(setTagsMock).toHaveBeenCalledTimes(1);
- expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK);
+ expect(setTagMock).toHaveBeenCalledTimes(2);
+ expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1');
+ expect(setTagMock).toHaveBeenCalledWith('tag2', true);
+
+ expect(setMeasurementMock).toHaveBeenCalledTimes(1);
+ expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none');
});
});
diff --git a/app/util/trace.ts b/app/util/trace.ts
index 8275c521b84..05ad23d9dd8 100644
--- a/app/util/trace.ts
+++ b/app/util/trace.ts
@@ -2,15 +2,19 @@ import {
startSpan as sentryStartSpan,
startSpanManual,
withScope,
+ setMeasurement,
+ Scope,
} from '@sentry/react-native';
import performance from 'react-native-performance';
-import type { Primitive, Span, StartSpanOptions } from '@sentry/types';
+import type { Span, StartSpanOptions, MeasurementUnit } from '@sentry/types';
import { createModuleLogger, createProjectLogger } from '@metamask/utils';
// Cannot create this 'sentry' logger in Sentry util file because of circular dependency
const projectLogger = createProjectLogger('sentry');
const log = createModuleLogger(projectLogger, 'trace');
-
+/**
+ * The supported trace names.
+ */
export enum TraceName {
DeveloperTest = 'Developer Test',
Middleware = 'Middleware',
@@ -19,6 +23,29 @@ export enum TraceName {
NotificationDisplay = 'Notification Display',
PPOMValidation = 'PPOM Validation',
Signature = 'Signature',
+ LoadScripts = 'Load Scripts',
+ SetupStore = 'Setup Store',
+ LoginToPasswordEntry = 'Login to Password Entry',
+ AuthenticateUser = 'Authenticate User',
+ BiometricAuthentication = 'Biometrics Authentication',
+ EngineInitialization = 'Engine Initialization',
+ CreateStore = 'Create Store',
+ CreateNewWalletToChoosePassword = 'Create New Wallet to Choose Password',
+ StorageRehydration = 'Storage Rehydration',
+ UIStartup = 'Custom UIStartup',
+}
+
+export enum TraceOperation {
+ LoadScripts = 'custom.load.scripts',
+ SetupStore = 'custom.setup.store',
+ LoginToPasswordEntry = 'custom.login.to.password.entry',
+ BiometricAuthentication = 'biometrics.authentication',
+ AuthenticateUser = 'custom.authenticate.user',
+ EngineInitialization = 'custom.engine.initialization',
+ CreateStore = 'custom.create.store',
+ CreateNewWalletToChoosePassword = 'custom.create.new.wallet',
+ StorageRehydration = 'custom.storage.rehydration',
+ UIStartup = 'custom.ui.startup',
}
const ID_DEFAULT = 'default';
@@ -33,23 +60,72 @@ export interface PendingTrace {
startTime: number;
timeoutId: NodeJS.Timeout;
}
-
+/**
+ * A context object to associate traces with each other and generate nested traces.
+ */
export type TraceContext = unknown;
-
+/**
+ * A callback function that can be traced.
+ */
export type TraceCallback = (context?: TraceContext) => T;
-
+/**
+ * A request to create a new trace.
+ */
export interface TraceRequest {
+ /**
+ * Custom data to associate with the trace.
+ */
data?: Record;
+
+ /**
+ * A unique identifier when not tracing a callback.
+ * Defaults to 'default' if not provided.
+ */
id?: string;
+
+ /**
+ * The name of the trace.
+ */
name: TraceName;
+
+ /**
+ * The parent context of the trace.
+ * If provided, the trace will be nested under the parent trace.
+ */
parentContext?: TraceContext;
+
+ /**
+ * Override the start time of the trace.
+ */
startTime?: number;
+
+ /**
+ * Custom tags to associate with the trace.
+ */
tags?: Record;
+ /**
+ * Custom operation name to associate with the trace.
+ */
+ op?: string;
}
-
+/**
+ * A request to end a pending trace.
+ */
export interface EndTraceRequest {
+ /**
+ * The unique identifier of the trace.
+ * Defaults to 'default' if not provided.
+ */
id?: string;
+
+ /**
+ * The name of the trace.
+ */
name: TraceName;
+
+ /**
+ * Override the end time of the trace.
+ */
timestamp?: number;
}
@@ -57,6 +133,16 @@ export function trace(request: TraceRequest, fn: TraceCallback): T;
export function trace(request: TraceRequest): TraceContext;
+/**
+ * Create a Sentry transaction to analyse the duration of a code flow.
+ * If a callback is provided, the transaction will be automatically ended when the callback completes.
+ * If the callback returns a promise, the transaction will be ended when the promise resolves or rejects.
+ * If no callback is provided, the transaction must be manually ended using `endTrace`.
+ *
+ * @param request - The data associated with the trace, such as the name and tags.
+ * @param fn - The optional callback to record the duration of.
+ * @returns The context of the trace, or the result of the callback if provided.
+ */
export function trace(
request: TraceRequest,
fn?: TraceCallback,
@@ -68,6 +154,12 @@ export function trace(
return traceCallback(request, fn);
}
+/**
+ * End a pending trace that was started without a callback.
+ * Does nothing if the pending trace cannot be found.
+ *
+ * @param request - The data necessary to identify and end the pending trace.
+ */
export function endTrace(request: EndTraceRequest) {
const { name, timestamp } = request;
const id = getTraceId(request);
@@ -100,6 +192,10 @@ function traceCallback(request: TraceRequest, fn: TraceCallback): T {
const start = Date.now();
let error: unknown;
+ if (span) {
+ initSpan(span, request);
+ }
+
return tryCatchMaybePromise(
() => fn(span),
(currentError) => {
@@ -130,6 +226,10 @@ function startTrace(request: TraceRequest): TraceContext {
span?.end(timestamp);
};
+ if (span) {
+ initSpan(span, request);
+ }
+
const timeoutId = setTimeout(() => {
log('Trace cleanup due to timeout', name, id);
end();
@@ -154,13 +254,13 @@ function startSpan(
request: TraceRequest,
callback: (spanOptions: StartSpanOptions) => T,
) {
- const { data: attributes, name, parentContext, startTime, tags } = request;
+ const { data: attributes, name, parentContext, startTime, op } = request;
const parentSpan = (parentContext ?? null) as Span | null;
const spanOptions: StartSpanOptions = {
attributes,
name,
- op: OP_DEFAULT,
+ op: op || OP_DEFAULT,
// This needs to be parentSpan once we have the withIsolatedScope implementation in place in the Sentry SDK for React Native
// Reference PR that updates @sentry/react-native: https://github.com/getsentry/sentry-react-native/pull/3895
parentSpanId: parentSpan?.spanId,
@@ -168,7 +268,7 @@ function startSpan(
};
return withScope((scope) => {
- scope.setTags(tags as Record);
+ initScope(scope, request);
return callback(spanOptions);
}) as T;
@@ -185,6 +285,40 @@ function getTraceKey(request: TraceRequest) {
return [name, id].join(':');
}
+/**
+ * Initialise the isolated Sentry scope created for each trace.
+ * Includes setting all non-numeric tags.
+ *
+ * @param scope - The Sentry scope to initialise.
+ * @param request - The trace request.
+ */
+function initScope(scope: Scope, request: TraceRequest) {
+ const tags = request.tags ?? {};
+
+ for (const [key, value] of Object.entries(tags)) {
+ if (typeof value !== 'number') {
+ scope.setTag(key, value);
+ }
+ }
+}
+
+/**
+ * Initialise the Sentry span created for each trace.
+ * Includes setting all numeric tags as measurements so they can be queried numerically in Sentry.
+ *
+ * @param _span - The Sentry span to initialise.
+ * @param request - The trace request.
+ */
+function initSpan(_span: Span, request: TraceRequest) {
+ const tags = request.tags ?? {};
+
+ for (const [key, value] of Object.entries(tags)) {
+ if (typeof value === 'number') {
+ sentrySetMeasurement(key, value, 'none');
+ }
+ }
+}
+
function getPerformanceTimestamp(): number {
return performance.timeOrigin + performance.now();
}
@@ -217,3 +351,11 @@ function tryCatchMaybePromise(
return undefined;
}
+
+function sentrySetMeasurement(
+ key: string,
+ value: number,
+ unit: MeasurementUnit,
+) {
+ setMeasurement(key, value, unit);
+}
diff --git a/bitrise.yml b/bitrise.yml
index 4df0c76e2c4..40c4e621f3b 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -135,7 +135,7 @@ stages:
- run_ios_api_specs: {}
- run_tag_smoke_accounts_ios: {}
- run_tag_smoke_accounts_android: {}
- - run_tag_smoke_assets_ios: {}
+ # - run_tag_smoke_assets_ios: {}
- run_tag_smoke_assets_android: {}
- run_tag_smoke_confirmations_ios: {}
- run_tag_smoke_confirmations_android: {}
@@ -145,8 +145,8 @@ stages:
- run_tag_smoke_core_android: {}
build_regression_e2e_ios_android_stage:
workflows:
- - ios_build_regression_tests: {}
- - android_build_regression_tests: {}
+ - ios_e2e_build: {}
+ - android_e2e_build: {}
run_regression_e2e_ios_android_stage:
workflows:
- ios_run_regression_tests: {}
@@ -267,8 +267,29 @@ workflows:
source "${HOME}/.nvm/nvm.sh"
echo 'source "${HOME}/.nvm/nvm.sh"' | tee -a ${HOME}/.{bashrc,profile}
- nvm install ${NODE_VERSION}
+ # Retry logic for Node installation
+ MAX_ATTEMPTS=3
+ ATTEMPT=1
+ until [ $ATTEMPT -gt $MAX_ATTEMPTS ]
+ do
+ echo "Attempt $ATTEMPT to install Node.js"
+ nvm install ${NODE_VERSION}
+ INSTALL_STATUS=$? # Capture the exit status of the nvm install command
+ if [ $INSTALL_STATUS -eq 0 ]; then
+ echo "Node.js installation successful!"
+ break
+ else
+ echo "Node.js installation failed with exit code $INSTALL_STATUS"
+ ATTEMPT=$((ATTEMPT+1))
+ echo "Node.js installation failed, retrying in 5 seconds..."
+ sleep 5
+ fi
+ done
+ if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then
+ echo "Node.js installation failed after $MAX_ATTEMPTS attempts."
+ exit 1
+ fi
envman add --key PATH --value $PATH
node --version
@@ -586,6 +607,10 @@ workflows:
inputs:
- ndk_version: $NDK_VERSION
- gradlew_path: $PROJECT_LOCATION/gradlew
+ - file-downloader@1:
+ inputs:
+ - source: $BITRISEIO_ANDROID_QA_KEYSTORE_URL
+ - destination: android/keystores/internalRelease.keystore
- script@1:
title: Install CCache & symlink
inputs:
@@ -620,7 +645,7 @@ workflows:
node -v
export METAMASK_ENVIRONMENT='local'
export METAMASK_BUILD_TYPE='main'
- IGNORE_BOXLOGS_DEVELOPMENT="true" FORCE_BUNDLING=true yarn test:e2e:android:bitrise:build
+ IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:android:build:qa-release
- save-gradle-cache@1: {}
- save-cache@1:
title: Save CCache
@@ -700,7 +725,7 @@ workflows:
fi
export METAMASK_ENVIRONMENT='local'
export METAMASK_BUILD_TYPE='main'
- IGNORE_BOXLOGS_DEVELOPMENT="true" FORCE_BUNDLING=true yarn test:e2e:android:bitrise:run "$TEST_SUITE_FOLDER" --testNamePattern="$TEST_SUITE"
+ IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:android:run:qa-release "$TEST_SUITE_FOLDER" --testNamePattern="$TEST_SUITE"
- custom-test-results-export@1:
title: Export test results
is_always_run: true
@@ -802,6 +827,8 @@ workflows:
- deploy_path: $BITRISE_HTML_REPORT_DIR
title: Deploy test report files
ios_e2e_build:
+ envs:
+ - NO_FLIPPER: '1'
before_run:
- install_applesimutils
- code_setup
@@ -833,7 +860,7 @@ workflows:
inputs:
- content: |-
#!/usr/bin/env bash
- HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 brew install ccache
+ brew install ccache with HOMEBREW_NO_DEPENDENTS_CHECK=1
ln -s $(which ccache) /usr/local/bin/gcc
ln -s $(which ccache) /usr/local/bin/g++
ln -s $(which ccache) /usr/local/bin/cc
@@ -867,7 +894,7 @@ workflows:
node -v
export METAMASK_ENVIRONMENT='local'
export METAMASK_BUILD_TYPE='main'
- IGNORE_BOXLOGS_DEVELOPMENT="true" FORCE_BUNDLING=true yarn test:e2e:ios:debug:build
+ IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:ios:build:qa-release
- save-cocoapods-cache@1: {}
- save-cache@1:
title: Save CCache
@@ -888,6 +915,8 @@ workflows:
- key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }}
- paths: node_modules
ios_e2e_test:
+ envs:
+ - NO_FLIPPER: '1'
before_run:
- setup
- install_applesimutils
@@ -923,7 +952,7 @@ workflows:
- set-xcode-build-number@1:
inputs:
- build_short_version_string: $VERSION_NAME
- - plist_path: $PROJECT_LOCATION_IOS/MetaMask/Info.plist
+ - plist_path: $PROJECT_LOCATION_IOS/MetaMask/MetaMask-QA-Info.plist
- script:
inputs:
- content: |-
@@ -954,7 +983,7 @@ workflows:
node -v
export METAMASK_ENVIRONMENT='local'
export METAMASK_BUILD_TYPE='main'
- IGNORE_BOXLOGS_DEVELOPMENT="true" FORCE_BUNDLING=true yarn test:e2e:ios:debug:run "$TEST_SUITE_FOLDER" --testNamePattern="$TEST_SUITE"
+ IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:ios:run:qa-release "$TEST_SUITE_FOLDER" --testNamePattern="$TEST_SUITE"
- custom-test-results-export@1:
is_always_run: true
is_skippable: false
@@ -1234,8 +1263,6 @@ workflows:
inputs:
- ipa_path: $BITRISE_APP_STORE_IPA_PATH
build_ios_release:
- envs:
- - NO_FLIPPER: '1'
before_run:
- code_setup
after_run:
@@ -1275,8 +1302,6 @@ workflows:
- deploy_path: sourcemaps/ios/index.js.map
title: Deploy Source Map
build_ios_qa:
- envs:
- - NO_FLIPPER: '1'
before_run:
- code_setup
after_run:
diff --git a/docs/readme/testing.md b/docs/readme/testing.md
index 546686c39e0..c79373ddc92 100644
--- a/docs/readme/testing.md
+++ b/docs/readme/testing.md
@@ -357,4 +357,4 @@ Our CI/CD process is automated through various Bitrise pipelines, each designed
### Best Practices
-For more guidelines and best practices, refer to our [Best Practices Document](https://github.com/MetaMask/contributor-docs/blob/main/docs/e2e-testing.md).
+For more guidelines and best practices, refer to our [Best Practices Document](https://github.com/MetaMask/contributor-docs/blob/main/docs/testing/e2e-testing.md).
diff --git a/e2e/fixtures/fixture-builder.js b/e2e/fixtures/fixture-builder.js
index c4e987b3f01..b850fab5970 100644
--- a/e2e/fixtures/fixture-builder.js
+++ b/e2e/fixtures/fixture-builder.js
@@ -421,6 +421,18 @@ class FixtureBuilder {
pendingApprovalCount: 0,
approvalFlows: [],
},
+ NotificationServicesController: {
+ subscriptionAccountsSeen: [],
+ isMetamaskNotificationsFeatureSeen: false,
+ isNotificationServicesEnabled: false,
+ isFeatureAnnouncementsEnabled: false,
+ metamaskNotificationsList: [],
+ metamaskNotificationsReadList: [],
+ isUpdatingMetamaskNotifications: false,
+ isFetchingMetamaskNotifications: false,
+ isUpdatingMetamaskNotificationsAccount: [],
+ isCheckingAccountsPresence: false,
+ },
},
},
privacy: {
diff --git a/e2e/specs/assets/nft-detection-modal.spec.js b/e2e/specs/assets/nft-detection-modal.spec.js
index b0b4b4e41ce..798b9267e3a 100644
--- a/e2e/specs/assets/nft-detection-modal.spec.js
+++ b/e2e/specs/assets/nft-detection-modal.spec.js
@@ -10,12 +10,10 @@ import TestHelpers from '../../helpers';
import Assertions from '../../utils/Assertions';
import NftDetectionModal from '../../pages/modals/NftDetectionModal';
import { SmokeAssets } from '../../tags';
-import NetworkListModal from '../../pages/modals/NetworkListModal';
-import NetworkEducationModal from '../../pages/modals/NetworkEducationModal';
+
import { NftDetectionModalSelectorsText } from '../../selectors/Modals/NftDetectionModal.selectors';
describe(SmokeAssets('NFT Detection Modal'), () => {
- const ETHEREUM = 'Ethereum Main Network';
beforeAll(async () => {
jest.setTimeout(170000);
await TestHelpers.reverseServerPort();
@@ -25,7 +23,6 @@ describe(SmokeAssets('NFT Detection Modal'), () => {
await withFixtures(
{
fixture: new FixtureBuilder()
- .withGanacheNetwork()
.withPreferencesController({
useNftDetection: false,
})
@@ -35,12 +32,6 @@ describe(SmokeAssets('NFT Detection Modal'), () => {
},
async () => {
await loginToApp();
-
- // Switch to Mainnet
- await WalletView.tapNetworksButtonOnNavBar();
- await NetworkListModal.changeNetworkTo(ETHEREUM);
- await NetworkEducationModal.tapGotItButton();
-
await Assertions.checkIfVisible(NftDetectionModal.container);
// fix flaky test: toast should desapear to get access to cancel button
@@ -65,7 +56,6 @@ describe(SmokeAssets('NFT Detection Modal'), () => {
await withFixtures(
{
fixture: new FixtureBuilder()
- .withGanacheNetwork()
.withPreferencesController({
useNftDetection: false,
})
@@ -76,11 +66,6 @@ describe(SmokeAssets('NFT Detection Modal'), () => {
async () => {
await loginToApp();
- // Switch to Mainnet
- await WalletView.tapNetworksButtonOnNavBar();
- await NetworkListModal.changeNetworkTo(ETHEREUM);
- await NetworkEducationModal.tapGotItButton();
-
await Assertions.checkIfVisible(NftDetectionModal.container);
await NftDetectionModal.tapAllowButton();
// Check that we are on the wallet screen
diff --git a/ios/Podfile b/ios/Podfile
index 759ce5f2e02..56b8c3f9164 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -98,7 +98,7 @@ def common_target_logic
pod 'GzipSwift'
# Pod for fixing react-native-quick-crypto issue: https://github.com/margelo/react-native-quick-crypto/issues/244
- pod "OpenSSL-Universal", "= 1.1.1100"
+ pod 'OpenSSL-Universal', :modular_headers => true, :configurations => ['Release']
end
target 'MetaMask' do
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b8151245f04..24d83fa9fef 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -702,8 +702,6 @@ PODS:
- React-Core
- RNCPicker (2.2.1):
- React-Core
- - RNDateTimePicker (7.7.0):
- - React-Core
- RNDefaultPreference (1.4.3):
- React
- RNDeviceInfo (9.0.2):
@@ -825,6 +823,7 @@ DEPENDENCIES:
- GzipSwift
- lottie-ios (from `../node_modules/lottie-ios`)
- lottie-react-native (from `../node_modules/lottie-react-native`)
+ - OpenSSL-Universal
- OpenSSL-Universal (= 1.1.1100)
- Permission-BluetoothPeripheral (from `../node_modules/react-native-permissions/ios/BluetoothPeripheral`)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
@@ -897,7 +896,6 @@ DEPENDENCIES:
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
- - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
- RNDefaultPreference (from `../node_modules/react-native-default-preference`)
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- "RNFBApp (from `../node_modules/@react-native-firebase/app`)"
@@ -1115,8 +1113,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-masked-view/masked-view"
RNCPicker:
:path: "../node_modules/@react-native-picker/picker"
- RNDateTimePicker:
- :path: "../node_modules/@react-native-community/datetimepicker"
RNDefaultPreference:
:path: "../node_modules/react-native-default-preference"
RNDeviceInfo:
@@ -1274,7 +1270,6 @@ SPEC CHECKSUMS:
RNCClipboard: ddd4d291537f1667209c9c405aaa4307297e252e
RNCMaskedView: 090213d32d8b3bb83a4dcb7d12c18f0152591906
RNCPicker: cb57c823d5ce8d2d0b5dfb45ad97b737260dc59e
- RNDateTimePicker: 4f3c4dbd4f908be32ec8c93f086e8924bd4a2e07
RNDefaultPreference: 2f8d6d54230edbd78708ada8d63bb275e5a8415b
RNDeviceInfo: 1e3f62b9ec32f7754fac60bd06b8f8a27124e7f0
RNFBApp: 5f87753a8d8b37d229adf85cd0ff37709ffdf008
@@ -1302,6 +1297,6 @@ SPEC CHECKSUMS:
Yoga: 6f5ab94cd8b1ecd04b6e973d0bc583ede2a598cc
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
-PODFILE CHECKSUM: 876298d4a106643492005466f7a314cd08711f4d
+PODFILE CHECKSUM: e0bcc4eb12d48746028cd4f4161a292fa9ddc627
COCOAPODS: 1.15.2
diff --git a/locales/languages/en.json b/locales/languages/en.json
index e4c5e637b3e..e2bbea8e5c3 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -3351,7 +3351,14 @@
"approximately": "approximately"
},
"unstake_input_banner_description":"On average, it takes less than 3 days for the unstaked ETH to be claimable, but can take up to 11 days.",
- "max": "Max"
+ "max": "Max",
+ "staking_from": "Staking from",
+ "interacting_with": "Interacting with",
+ "12_hours": "12 hours",
+ "terms_of_service": "Terms of service",
+ "risk_disclosure": "Risk disclosure",
+ "cancel": "Cancel",
+ "confirm": "Confirm"
},
"default_settings": {
"title": "Your Wallet is ready",
@@ -3416,6 +3423,16 @@
"reward_rate": {
"title": "Reward rate",
"tooltip": "Expected yearly increase in the value of your stake, based on the reward rate over the past week."
+ },
+ "estimated_gas_fee": {
+ "title": "Estimated gas fee",
+ "gas_recipient": "Gas fees are paid to crypto miners who process transactions on Ethereum network. Metamask does not profit from gas fees.",
+ "gas_fluctuation": "Gas fees are estimated and will fluctuate based on network traffic and transaction complexity.",
+ "gas_learn_more": "Learn more about gas fees"
+ },
+ "reward_frequency": {
+ "title": "Reward frequency",
+ "tooltip": "Your staked balance updates every 12 hours to account for new rewards."
}
},
"confirm": {
diff --git a/package.json b/package.json
index 85e3ee03801..67b20e24b93 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,6 @@
"start:android:flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/build.sh android flaskDebug",
"build:announce": "node ./scripts/metamask-bot-build-announce-bitrise.js",
"build:android:release": "./scripts/build.sh android release",
- "build:android:release:e2e": "./scripts/build.sh android releaseE2E",
- "build:android:qa:e2e": "./scripts/build.sh android QAE2E",
"build:android:checksum": "./scripts/checksum.sh",
"build:android:checksum:qa": "./scripts/checksum.sh QA",
"build:android:checksum:flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/checksum.sh flask",
@@ -44,28 +42,26 @@
"build:android:pre-release:bundle:flask": "export METAMASK_BUILD_TYPE='flask' && GENERATE_BUNDLE=true ./scripts/build.sh android flask --pre",
"build:ios:release": "./scripts/build.sh ios release",
"build:ios:pre-flask": "export METAMASK_BUILD_TYPE='flask' && ./scripts/build.sh ios flask --pre",
- "build:ios:release:e2e": "./scripts/build.sh ios releaseE2E",
"build:ios:pre-release": "./scripts/build.sh ios release --pre",
- "build:ios:qa": "./scripts/build.sh ios QA",
"build:ios:pre-qa": "./scripts/build.sh ios QA --pre",
+ "build:android:qa": "NO_FLIPPER='1' ./scripts/build.sh android QA",
+ "build:ios:qa": "NO_FLIPPER='1' ./scripts/build.sh ios QA",
"build:attribution": "./scripts/generate-attributions.sh",
"release:android": "./scripts/build.sh android release && open android/app/build/outputs/apk/release/",
"release:ios": "./scripts/build.sh ios release",
"release:android:qa": "./scripts/build.sh android QA && open android/app/build/outputs/apk/release/",
- "test": "yarn test:unit && yarn test:e2e",
+ "test": "yarn test:unit",
"test:unit": "jest ./app/ ./locales/",
"test:unit:update": "time jest -u ./app/",
"test:api-specs": "detox reset-lock-file && detox test -c ios.sim.apiSpecs",
- "test:e2e": "yarn test:e2e:ios && yarn test:e2e:android",
- "test:e2e:ios": "detox build -c ios.sim.release && detox test -c ios.sim.release",
+ "test:e2e:ios:build:qa-release": "IS_TEST='true' detox build -c ios.sim.qa",
+ "test:e2e:ios:run:qa-release": "IS_TEST='true' detox test -c ios.sim.qa",
+ "test:e2e:android:build:qa-release": "NO_FLIPPER='1' IS_TEST='true' detox build -c android.emu.release.qa",
+ "test:e2e:android:run:qa-release": "NO_FLIPPER='1' IS_TEST='true' detox test -c android.emu.release.qa --headless --record-logs all",
"test:e2e:ios:debug:build": "IS_TEST='true' detox build -c ios.sim.debug",
"test:e2e:ios:debug:run": "IS_TEST='true' detox reset-lock-file && detox test -c ios.sim.debug",
"test:e2e:android:debug:build": "IS_TEST='true' detox build -c android.emu.debug",
- "test:e2e:android:bitrise:build": "IS_TEST='true' detox build -c android.emu.bitrise.debug",
"test:e2e:android:debug:run": "IS_TEST='true' detox test -c android.emu.debug",
- "test:e2e:android:bitrise:run": "IS_TEST='true' detox reset-lock-file && detox test -c android.emu.bitrise.debug --headless",
- "test:e2e:android": "detox build -c android.emu.release && detox test -c android.emu.release --record-videos failing",
- "test:e2e:android:qa": "detox build -c android.emu.release.qa && detox test -c android.emu.release.qa --record-videos failing",
"test:wdio:ios": "yarn wdio ./wdio/config/ios.config.debug.js",
"test:wdio:ios:browserstack:local": "yarn wdio ./wdio/config/ios.config.browserstack.local.js",
"test:wdio:android": "yarn wdio ./wdio/config/android.config.debug.js",
@@ -136,7 +132,7 @@
"send": "0.19.0"
},
"dependencies": {
- "@consensys/on-ramp-sdk": "1.28.3",
+ "@consensys/on-ramp-sdk": "1.28.5",
"@ethersproject/abi": "^5.7.0",
"@keystonehq/bc-ur-registry-eth": "^0.19.1",
"@keystonehq/metamask-airgapped-keyring": "^0.13.1",
@@ -145,7 +141,7 @@
"@metamask/accounts-controller": "^18.2.1",
"@metamask/address-book-controller": "^6.0.1",
"@metamask/approval-controller": "^7.0.1",
- "@metamask/assets-controllers": "^31.0.0",
+ "@metamask/assets-controllers": "^32.0.0",
"@metamask/base-controller": "^7.0.1",
"@metamask/composable-controller": "^3.0.0",
"@metamask/contract-metadata": "^2.1.0",
@@ -197,7 +193,6 @@
"@react-native-clipboard/clipboard": "1.8.4",
"@react-native-community/blur": "^4.4.0",
"@react-native-community/checkbox": "^0.5.17",
- "@react-native-community/datetimepicker": "^7.5.0",
"@react-native-community/netinfo": "^9.5.0",
"@react-native-community/slider": "^4.4.3",
"@react-native-cookies/cookies": "^6.2.1",
@@ -477,7 +472,6 @@
"prettier": "^2.2.1",
"prettier-plugin-gherkin": "^1.1.1",
"react-dom": "18.2.0",
- "react-native-cli": "2.0.1",
"react-native-flipper": "^0.263.0",
"react-native-launch-arguments": "^4.0.1",
"react-native-performance": "^5.1.2",
diff --git a/patches/@metamask+assets-controllers++@metamask+preferences-controller+12.0.0.patch b/patches/@metamask+assets-controllers++@metamask+preferences-controller+13.0.3.patch
similarity index 54%
rename from patches/@metamask+assets-controllers++@metamask+preferences-controller+12.0.0.patch
rename to patches/@metamask+assets-controllers++@metamask+preferences-controller+13.0.3.patch
index 00f4da63438..c45e6f5109a 100644
--- a/patches/@metamask+assets-controllers++@metamask+preferences-controller+12.0.0.patch
+++ b/patches/@metamask+assets-controllers++@metamask+preferences-controller+13.0.3.patch
@@ -1,7 +1,7 @@
-diff --git a/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts b/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts
-index ddf6eb4..e8dac6d 100644
---- a/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts
-+++ b/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/types/PreferencesController.d.ts
+diff --git a/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/PreferencesController.d.cts b/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/PreferencesController.d.cts
+index 04a9d6f..391652d 100644
+--- a/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/PreferencesController.d.cts
++++ b/node_modules/@metamask/assets-controllers/node_modules/@metamask/preferences-controller/dist/PreferencesController.d.cts
@@ -65,7 +65,7 @@ export type PreferencesState = {
/**
* Controls whether the OpenSea API is used
diff --git a/patches/@metamask+assets-controllers+31.0.0.patch b/patches/@metamask+assets-controllers+32.0.0.patch
similarity index 77%
rename from patches/@metamask+assets-controllers+31.0.0.patch
rename to patches/@metamask+assets-controllers+32.0.0.patch
index 7de2ffecb6a..875d616b53e 100644
--- a/patches/@metamask+assets-controllers+31.0.0.patch
+++ b/patches/@metamask+assets-controllers+32.0.0.patch
@@ -1,10 +1,10 @@
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-4AC3X2U5.js b/node_modules/@metamask/assets-controllers/dist/chunk-4AC3X2U5.js
-index bb55790..b235cbf 100644
+index bb55790..c7e7f99 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-4AC3X2U5.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-4AC3X2U5.js
-@@ -292,6 +292,18 @@ var TokensController = class extends _basecontroller.BaseController {
- releaseLock();
- }
+@@ -187,6 +187,18 @@ var TokensController = class extends _basecontroller.BaseController {
+ }
+ );
}
+ /**
+ * THIS FUNCTIONS IS CURRENTLY PATCHED AND STILL NEEDS TO BE IMPLEMENTED ON THE CORE REPO
@@ -19,9 +19,9 @@ index bb55790..b235cbf 100644
+ });
+ }
/**
- * Add a batch of tokens.
+ * Adds a token to the stored token list.
*
-@@ -605,9 +617,14 @@ _selectedAddress = new WeakMap();
+@@ -605,9 +617,13 @@ _selectedAddress = new WeakMap();
_provider = new WeakMap();
_abortController = new WeakMap();
_onNetworkDidChange = new WeakSet();
@@ -29,7 +29,6 @@ index bb55790..b235cbf 100644
+onNetworkDidChange_fn = function({ selectedNetworkClientId }) {
const { allTokens, allIgnoredTokens, allDetectedTokens } = this.state;
- const { chainId } = providerConfig;
-+ // This wont be needed in v32
+ const selectedNetworkClient = this.messagingSystem.call(
+ 'NetworkController:getNetworkClientById',
+ selectedNetworkClientId,
@@ -39,22 +38,10 @@ index bb55790..b235cbf 100644
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _abortController, new AbortController());
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _chainId, chainId);
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-CGLUTXI7.js b/node_modules/@metamask/assets-controllers/dist/chunk-CGLUTXI7.js
-index 7cc44fa..a7663b4 100644
+index 7cc44fa..7a1de65 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-CGLUTXI7.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-CGLUTXI7.js
-@@ -17,7 +17,10 @@ var _basecontroller = require('@metamask/base-controller');
-
-
-
--
-+/**
-+ * Changes regarding displayNftMedia, TokenURI and error nft metadata property are not on the core repo and needed to be refactor to be removed from the patch
-+ * updateNftMetadata changes will be introduced on latest versions of changes of assets controllers, v^30 or next
-+ */
-
-
-
-@@ -44,7 +47,7 @@ var getDefaultNftControllerState = () => ({
+@@ -44,7 +44,7 @@ var getDefaultNftControllerState = () => ({
allNfts: {},
ignoredNfts: []
});
@@ -63,7 +50,7 @@ index 7cc44fa..a7663b4 100644
var NftController = class extends _basecontroller.BaseController {
/**
* Creates an NftController instance.
-@@ -53,7 +56,7 @@ var NftController = class extends _basecontroller.BaseController {
+@@ -53,7 +53,7 @@ var NftController = class extends _basecontroller.BaseController {
* @param options.chainId - The chain ID of the current network.
* @param options.selectedAddress - The currently selected address.
* @param options.ipfsGateway - The configured IPFS gateway.
@@ -72,7 +59,7 @@ index 7cc44fa..a7663b4 100644
* @param options.useIpfsSubdomains - Controls whether IPFS subdomains are used.
* @param options.isIpfsGatewayEnabled - Controls whether IPFS is enabled or not.
* @param options.getERC721AssetName - Gets the name of the asset at the given address.
-@@ -71,7 +74,7 @@ var NftController = class extends _basecontroller.BaseController {
+@@ -71,7 +71,7 @@ var NftController = class extends _basecontroller.BaseController {
chainId: initialChainId,
selectedAddress = "",
ipfsGateway = _controllerutils.IPFS_DEFAULT_GATEWAY_URL,
@@ -81,7 +68,7 @@ index 7cc44fa..a7663b4 100644
useIpfsSubdomains = true,
isIpfsGatewayEnabled = true,
getERC721AssetName,
-@@ -104,7 +107,7 @@ var NftController = class extends _basecontroller.BaseController {
+@@ -104,7 +104,7 @@ var NftController = class extends _basecontroller.BaseController {
* @param preferencesState - The new state of the preference controller.
* @param preferencesState.selectedAddress - The current selected address.
* @param preferencesState.ipfsGateway - The configured IPFS gateway.
@@ -90,7 +77,7 @@ index 7cc44fa..a7663b4 100644
* @param preferencesState.isIpfsGatewayEnabled - Controls whether IPFS is enabled or not.
*/
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _onPreferencesControllerStateChange);
-@@ -233,7 +236,7 @@ var NftController = class extends _basecontroller.BaseController {
+@@ -233,7 +233,7 @@ var NftController = class extends _basecontroller.BaseController {
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _selectedAddress, void 0);
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _chainId, void 0);
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _ipfsGateway, void 0);
@@ -99,7 +86,7 @@ index 7cc44fa..a7663b4 100644
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _useIpfsSubdomains, void 0);
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _isIpfsGatewayEnabled, void 0);
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _getERC721AssetName, void 0);
-@@ -246,7 +249,7 @@ var NftController = class extends _basecontroller.BaseController {
+@@ -246,7 +246,7 @@ var NftController = class extends _basecontroller.BaseController {
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _selectedAddress, selectedAddress);
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _chainId, initialChainId);
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _ipfsGateway, ipfsGateway);
@@ -108,7 +95,7 @@ index 7cc44fa..a7663b4 100644
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _useIpfsSubdomains, useIpfsSubdomains);
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _isIpfsGatewayEnabled, isIpfsGatewayEnabled);
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _getERC721AssetName, getERC721AssetName);
-@@ -268,6 +271,17 @@ var NftController = class extends _basecontroller.BaseController {
+@@ -268,6 +268,17 @@ var NftController = class extends _basecontroller.BaseController {
getNftApi() {
return `${_controllerutils.NFT_API_BASE_URL}/tokens`;
}
@@ -126,87 +113,7 @@ index 7cc44fa..a7663b4 100644
/**
* 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.
-@@ -430,43 +444,48 @@ var NftController = class extends _basecontroller.BaseController {
- userAddress = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _selectedAddress),
- networkClientId
- }) {
-- const chainId = _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getCorrectChainId, getCorrectChainId_fn).call(this, { networkClientId });
-- const nftsWithChecksumAdr = nfts.map((nft) => {
-- return {
-- ...nft,
-- address: _controllerutils.toChecksumHexAddress.call(void 0, nft.address)
-- };
-- });
-- const nftMetadataResults = await Promise.all(
-- nftsWithChecksumAdr.map(async (nft) => {
-- const resMetadata = await _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getNftInformation, getNftInformation_fn).call(this, nft.address, nft.tokenId, networkClientId);
-+ const releaseLock = await _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _mutex).acquire();
-+ try{
-+ const chainId = _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getCorrectChainId, getCorrectChainId_fn).call(this, { networkClientId });
-+ const nftsWithChecksumAdr = nfts.map((nft) => {
- return {
-- nft,
-- newMetadata: resMetadata
-+ ...nft,
-+ address: _controllerutils.toChecksumHexAddress.call(void 0, nft.address)
- };
-- })
-- );
-- const nftsWithDifferentMetadata = [];
-- const { allNfts } = this.state;
-- const stateNfts = allNfts[userAddress]?.[chainId] || [];
-- nftMetadataResults.forEach((singleNft) => {
-- const existingEntry = stateNfts.find(
-- (nft) => nft.address.toLowerCase() === singleNft.nft.address.toLowerCase() && nft.tokenId === singleNft.nft.tokenId
-+ });
-+ const nftMetadataResults = await Promise.all(
-+ nftsWithChecksumAdr.map(async (nft) => {
-+ const resMetadata = await _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getNftInformation, getNftInformation_fn).call(this, nft.address, nft.tokenId, networkClientId);
-+ return {
-+ nft,
-+ newMetadata: resMetadata
-+ };
-+ })
- );
-- if (existingEntry) {
-- const differentMetadata = _chunkNEXY7SE2js.compareNftMetadata.call(void 0,
-- singleNft.newMetadata,
-- existingEntry
-+ const nftsWithDifferentMetadata = [];
-+ const { allNfts } = this.state;
-+ const stateNfts = allNfts[userAddress]?.[chainId] || [];
-+ nftMetadataResults.forEach((singleNft) => {
-+ const existingEntry = stateNfts.find(
-+ (nft) => nft.address.toLowerCase() === singleNft.nft.address.toLowerCase() && nft.tokenId === singleNft.nft.tokenId
- );
-- if (differentMetadata) {
-- nftsWithDifferentMetadata.push(singleNft);
-+ if (existingEntry) {
-+ const differentMetadata = _chunkNEXY7SE2js.compareNftMetadata.call(void 0,
-+ singleNft.newMetadata,
-+ existingEntry
-+ );
-+ if (differentMetadata) {
-+ nftsWithDifferentMetadata.push(singleNft);
-+ }
- }
-+ });
-+ if (nftsWithDifferentMetadata.length !== 0) {
-+ nftsWithDifferentMetadata.forEach(
-+ (elm) => this.updateNft(elm.nft, elm.newMetadata, userAddress, chainId)
-+ );
- }
-- });
-- if (nftsWithDifferentMetadata.length !== 0) {
-- nftsWithDifferentMetadata.forEach(
-- (elm) => this.updateNft(elm.nft, elm.newMetadata, userAddress, chainId)
-- );
-+ } finally {
-+ releaseLock();
- }
- }
- /**
-@@ -771,7 +790,7 @@ _mutex = new WeakMap();
+@@ -771,7 +782,7 @@ _mutex = new WeakMap();
_selectedAddress = new WeakMap();
_chainId = new WeakMap();
_ipfsGateway = new WeakMap();
@@ -215,7 +122,7 @@ index 7cc44fa..a7663b4 100644
_useIpfsSubdomains = new WeakMap();
_isIpfsGatewayEnabled = new WeakMap();
_getERC721AssetName = new WeakMap();
-@@ -797,14 +816,14 @@ _onPreferencesControllerStateChange = new WeakSet();
+@@ -797,14 +808,14 @@ _onPreferencesControllerStateChange = new WeakSet();
onPreferencesControllerStateChange_fn = async function({
selectedAddress,
ipfsGateway,
@@ -233,7 +140,15 @@ index 7cc44fa..a7663b4 100644
if (needsUpdateNftMetadata) {
const nfts = this.state.allNfts[selectedAddress]?.[_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _chainId)] ?? [];
const nftsToUpdate = nfts.filter(
-@@ -850,12 +869,25 @@ getNftInformationFromApi_fn = async function(contractAddress, tokenId) {
+@@ -818,6 +829,7 @@ onPreferencesControllerStateChange_fn = async function({
+ }
+ }
+ };
++
+ _updateNestedNftState = new WeakSet();
+ updateNestedNftState_fn = function(newCollection, baseStateKey, { userAddress, chainId }) {
+ this.update((state) => {
+@@ -850,12 +862,25 @@ getNftInformationFromApi_fn = async function(contractAddress, tokenId) {
}
}
});
@@ -242,9 +157,9 @@ index 7cc44fa..a7663b4 100644
+ id: `${nftInformation?.tokens[0]?.token?.collection?.id}`
+ }).toString();
+ const collectionInformation = await _controllerutils.fetchWithErrorHandling.call(void 0, {
-+ url: `${_controllerutils.NFT_API_BASE_URL}/collections?${getCollectionParams}`,
-+ options: {
-+ headers: {
++ url: `${_controllerutils.NFT_API_BASE_URL}/collections?${getCollectionParams}`,
++ options: {
++ headers: {
+ Version: '1'
+ }
+ }
@@ -260,52 +175,25 @@ index 7cc44fa..a7663b4 100644
};
}
const {
-@@ -887,7 +919,16 @@ getNftInformationFromApi_fn = async function(contractAddress, tokenId) {
+@@ -887,7 +912,16 @@ getNftInformationFromApi_fn = async function(contractAddress, tokenId) {
},
rarityRank && { rarityRank },
rarity && { rarity },
- collection && { collection }
+ (collection || collectionInformation) && {
-+ collection: {
-+ ...collection || {},
-+ creator: collection?.creator || collectionInformation?.collections[0].creator,
-+ openseaVerificationStatus: collectionInformation?.collections[0].openseaVerificationStatus,
-+ contractDeployedAt: collectionInformation?.collections[0].contractDeployedAt,
-+ ownerCount: collectionInformation?.collections[0].ownerCount,
-+ topBid: collectionInformation?.collections[0].topBid
++ collection: {
++ ...collection || {},
++ creator: collection?.creator || collectionInformation?.collections[0].creator,
++ openseaVerificationStatus: collectionInformation?.collections[0].openseaVerificationStatus,
++ contractDeployedAt: collectionInformation?.collections[0].contractDeployedAt,
++ ownerCount: collectionInformation?.collections[0].ownerCount,
++ topBid: collectionInformation?.collections[0].topBid
++ }
+ }
-+ }
);
return nftMetadata;
};
-@@ -896,6 +937,17 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
- const result = await _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getNftURIAndStandard, getNftURIAndStandard_fn).call(this, contractAddress, tokenId, networkClientId);
- let tokenURI = result[0];
- const standard = result[1];
-+ if (!_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _displayNftMedia) && !_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _isIpfsGatewayEnabled)) {
-+ return {
-+ image: null,
-+ name: null,
-+ description: null,
-+ standard: standard || null,
-+ favorite: false,
-+ tokenURI,
-+ };
-+ }
-+
- const hasIpfsTokenURI = tokenURI.startsWith("ipfs://");
- if (hasIpfsTokenURI && !_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _isIpfsGatewayEnabled)) {
- return {
-@@ -907,7 +959,7 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
- tokenURI: tokenURI ?? null
- };
- }
-- const isDisplayNFTMediaToggleEnabled = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _openSeaEnabled);
-+ const isDisplayNFTMediaToggleEnabled = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _displayNftMedia);
- if (!hasIpfsTokenURI && !isDisplayNFTMediaToggleEnabled) {
- return {
- image: null,
-@@ -915,7 +967,8 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
+@@ -904,10 +938,11 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
description: null,
standard: standard || null,
favorite: false,
@@ -314,8 +202,12 @@ index 7cc44fa..a7663b4 100644
+ error: 'URI import error',
};
}
- if (hasIpfsTokenURI) {
-@@ -925,6 +978,16 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
+- const isDisplayNFTMediaToggleEnabled = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _openSeaEnabled);
++ const isDisplayNFTMediaToggleEnabled = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _displayNftMedia);
+ if (!hasIpfsTokenURI && !isDisplayNFTMediaToggleEnabled) {
+ return {
+ image: null,
+@@ -925,6 +960,16 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _useIpfsSubdomains)
);
}
@@ -332,7 +224,7 @@ index 7cc44fa..a7663b4 100644
try {
const object = await _controllerutils.handleFetch.call(void 0, tokenURI);
const image = Object.prototype.hasOwnProperty.call(object, "image") ? "image" : (
-@@ -946,7 +1009,8 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
+@@ -946,7 +991,8 @@ getNftInformationFromTokenURI_fn = async function(contractAddress, tokenId, netw
description: null,
standard: standard || null,
favorite: false,
@@ -342,7 +234,7 @@ index 7cc44fa..a7663b4 100644
};
}
};
-@@ -977,15 +1041,26 @@ getNftInformation_fn = async function(contractAddress, tokenId, networkClientId)
+@@ -977,15 +1023,27 @@ getNftInformation_fn = async function(contractAddress, tokenId, networkClientId)
_controllerutils.safelyExecute.call(void 0,
() => _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getNftInformationFromTokenURI, getNftInformationFromTokenURI_fn).call(this, contractAddress, tokenId, networkClientId)
),
@@ -351,16 +243,17 @@ index 7cc44fa..a7663b4 100644
() => _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getNftInformationFromApi, getNftInformationFromApi_fn).call(this, contractAddress, tokenId)
) : void 0
]);
++
+ if (blockchainMetadata?.error && nftApiMetadata?.error) {
-+ return {
-+ image: null,
-+ name: null,
-+ description: null,
-+ standard: blockchainMetadata.standard ?? null,
-+ favorite: false,
-+ tokenURI: blockchainMetadata.tokenURI ?? null,
-+ error: 'Both import failed',
-+ };
++ return {
++ image: null,
++ name: null,
++ description: null,
++ standard: blockchainMetadata.standard ?? null,
++ favorite: false,
++ tokenURI: blockchainMetadata.tokenURI ?? null,
++ error: 'Both import failed',
++ };
+ }
return {
...nftApiMetadata,
@@ -371,7 +264,7 @@ index 7cc44fa..a7663b4 100644
standard: blockchainMetadata?.standard ?? nftApiMetadata?.standard ?? null,
tokenURI: blockchainMetadata?.tokenURI ?? null
};
-@@ -1048,7 +1123,8 @@ addIndividualNft_fn = async function(tokenAddress, tokenId, nftMetadata, nftCont
+@@ -1048,7 +1106,8 @@ addIndividualNft_fn = async function(tokenAddress, tokenId, nftMetadata, nftCont
nftMetadata,
existingEntry
);
@@ -381,7 +274,7 @@ index 7cc44fa..a7663b4 100644
return;
}
const indexToUpdate = nfts.findIndex(
-@@ -1080,7 +1156,8 @@ addIndividualNft_fn = async function(tokenAddress, tokenId, nftMetadata, nftCont
+@@ -1080,7 +1139,8 @@ addIndividualNft_fn = async function(tokenAddress, tokenId, nftMetadata, nftCont
symbol: nftContract.symbol,
tokenId: tokenId.toString(),
standard: nftMetadata.standard,
@@ -392,7 +285,7 @@ index 7cc44fa..a7663b4 100644
}
} finally {
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-ELSMS5S7.js b/node_modules/@metamask/assets-controllers/dist/chunk-ELSMS5S7.js
-index 45254ad..cd5f3a1 100644
+index 45254ad..f3c6204 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-ELSMS5S7.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-ELSMS5S7.js
@@ -87,6 +87,7 @@ var CurrencyRateController = class extends _pollingcontroller.StaticIntervalPoll
@@ -426,7 +319,7 @@ index 45254ad..cd5f3a1 100644
- currentCurrency
- };
- });
-+ if (shouldUpdateState) {
++ if(shouldUpdateState) {
+ this.update(() => {
+ return {
+ currencyRates: {
@@ -439,13 +332,13 @@ index 45254ad..cd5f3a1 100644
+ },
+ currentCurrency
+ };
-+ })
++ });
+ }
} finally {
releaseLock();
}
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-GU53EI7A.js b/node_modules/@metamask/assets-controllers/dist/chunk-GU53EI7A.js
-index 33b048f..8815b95 100644
+index 33b048f..5867375 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-GU53EI7A.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-GU53EI7A.js
@@ -61,7 +61,7 @@ var AccountTrackerController = class extends _pollingcontroller.StaticIntervalPo
@@ -457,36 +350,34 @@ index 33b048f..8815b95 100644
const accountsForChain = { ...accountsByChainId[chainId] };
for (const address of accountsToUpdate) {
const balance = await this.getBalanceFromChain(address, ethQuery);
-@@ -80,9 +80,11 @@ var AccountTrackerController = class extends _pollingcontroller.StaticIntervalPo
+@@ -80,9 +80,8 @@ var AccountTrackerController = class extends _pollingcontroller.StaticIntervalPo
[chainId]: accountsForChain
}
});
- } catch (err) {
+ } finally {
-+ /**
-+ * This change is not present on the core repo
-+ */
releaseLock();
- throw err;
}
};
this.defaultConfig = {
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-HDI4L2DD.js b/node_modules/@metamask/assets-controllers/dist/chunk-HDI4L2DD.js
-index 76e3362..f733c85 100644
+index 76e3362..e11991d 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-HDI4L2DD.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-HDI4L2DD.js
-@@ -165,7 +165,9 @@ var TokenDetectionController = class extends _pollingcontroller.StaticIntervalPo
+@@ -165,7 +165,10 @@ var TokenDetectionController = class extends _pollingcontroller.StaticIntervalPo
if (!this.isActive) {
return;
}
- const addressAgainstWhichToDetect = selectedAddress ?? _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _selectedAddress);
++ //const addressAgainstWhichToDetect = selectedAddress ?? _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _selectedAddress);
+ const currentAddress = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _selectedAddress);
+ const currentAddressChecksum = _controllerutils.toChecksumHexAddress.call(void 0, currentAddress)
+ const addressAgainstWhichToDetect = _controllerutils.toChecksumHexAddress.call(void 0, selectedAddress) ?? currentAddressChecksum;
const { chainId, networkClientId: selectedNetworkClientId } = _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getCorrectChainIdAndNetworkClientId, getCorrectChainIdAndNetworkClientId_fn).call(this, networkClientId);
const chainIdAgainstWhichToDetect = chainId;
const networkClientIdAgainstWhichToDetect = selectedNetworkClientId;
-@@ -224,12 +226,10 @@ registerEventListeners_fn = function() {
+@@ -224,12 +227,10 @@ registerEventListeners_fn = function() {
);
this.messagingSystem.subscribe(
"PreferencesController:stateChange",
@@ -502,7 +393,7 @@ index 76e3362..f733c85 100644
selectedAddress: _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _selectedAddress)
});
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-IBK6AXPP.js b/node_modules/@metamask/assets-controllers/dist/chunk-IBK6AXPP.js
-index f7509a1..52bc67e 100644
+index f7509a1..4573718 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-IBK6AXPP.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-IBK6AXPP.js
@@ -19,7 +19,7 @@ function getDefaultTokenBalancesState() {
@@ -526,24 +417,23 @@ index f7509a1..52bc67e 100644
this.messagingSystem.subscribe(
"TokensController:stateChange",
({ tokens: newTokens, detectedTokens }) => {
-@@ -79,6 +81,16 @@ var TokenBalancesController = class extends _basecontroller.BaseController {
+@@ -79,6 +81,15 @@ var TokenBalancesController = class extends _basecontroller.BaseController {
disable() {
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _disabled, true);
}
-+
-+/**
++ /**
+ * THIS FUNCTIONS IS CURRENTLY PATCHED AND STILL NEEDS TO BE IMPLEMENTED ON THE CORE REPO
+ * Resets to the default state
+ */
+ reset() {
-+ this.update((state) => {
++ this.update((state) => {
+ state.contractBalances = {};
-+ });
-+ }
++ });
++ }
/**
* Starts a new polling interval.
*
-@@ -100,27 +112,34 @@ var TokenBalancesController = class extends _basecontroller.BaseController {
+@@ -100,27 +111,34 @@ var TokenBalancesController = class extends _basecontroller.BaseController {
* Updates balances for all tokens.
*/
async updateBalances() {
@@ -567,20 +457,20 @@ index f7509a1..52bc67e 100644
- }
- }
+ const balancePromises = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _tokens).map((token) => {
-+ const { address } = token;
++ const { address } = token;
+ return _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _getERC20BalanceOf).call(this, address, selectedAddress).then((balance) => {
-+ newContractBalances[address] = _controllerutils.toHex.call(void 0, balance);
-+ token = {
-+ ...token,
-+ hasBalanceError: false
-+ };
++ newContractBalances[address] = _controllerutils.toHex.call(void 0, balance);
++ token = {
++ ...token,
++ hasBalanceError: false
++ };
+ }).catch((error) => {
-+ newContractBalances[address] = _controllerutils.toHex.call(void 0, 0);
-+ token = {
-+ ...token,
-+ hasBalanceError: true
-+ };
-+ });
++ newContractBalances[address] = _controllerutils.toHex.call(void 0, 0);
++ token = {
++ ...token,
++ hasBalanceError: true
++ };
++ });
+ });
+ await Promise.all(balancePromises);
this.update((state) => {
@@ -590,7 +480,7 @@ index f7509a1..52bc67e 100644
}
};
_handle = new WeakMap();
-@@ -128,6 +147,7 @@ _getERC20BalanceOf = new WeakMap();
+@@ -128,6 +146,7 @@ _getERC20BalanceOf = new WeakMap();
_interval = new WeakMap();
_tokens = new WeakMap();
_disabled = new WeakMap();
@@ -621,38 +511,36 @@ index 44804c8..911a6e6 100644
this.clearingTokenListData();
} else {
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-QFDTOEYR.js b/node_modules/@metamask/assets-controllers/dist/chunk-QFDTOEYR.js
-index 5335fa5..ae37683 100644
+index 5335fa5..0854306 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-QFDTOEYR.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-QFDTOEYR.js
-@@ -7,6 +7,8 @@ var _chunkZ4BLTVTBjs = require('./chunk-Z4BLTVTB.js');
-
+@@ -8,12 +8,14 @@ var _chunkZ4BLTVTBjs = require('./chunk-Z4BLTVTB.js');
// src/NftDetectionController.ts
+
+-
+var utils_1 = require('@metamask/utils');
+var _chunkR4HATJKUjs = require('./chunk-NEXY7SE2.js');
-@@ -14,6 +16,7 @@ var _chunkZ4BLTVTBjs = require('./chunk-Z4BLTVTB.js');
-
var _controllerutils = require('@metamask/controller-utils');
+var supportedNftDetectionNetworks = [_controllerutils.ChainId.mainnet];
var _pollingcontroller = require('@metamask/polling-controller');
var DEFAULT_INTERVAL = 18e4;
var controllerName = "NftDetectionController";
-@@ -24,7 +27,9 @@ var BlockaidResultType = /* @__PURE__ */ ((BlockaidResultType2) => {
+@@ -24,7 +26,8 @@ var BlockaidResultType = /* @__PURE__ */ ((BlockaidResultType2) => {
BlockaidResultType2["Malicious"] = "Malicious";
return BlockaidResultType2;
})(BlockaidResultType || {});
-var _intervalId, _interval, _disabled, _addNft, _getNftState, _stopPolling, stopPolling_fn, _startPolling, startPolling_fn, _onPreferencesControllerStateChange, onPreferencesControllerStateChange_fn, _getOwnerNftApi, getOwnerNftApi_fn, _getOwnerNfts, getOwnerNfts_fn;
-+// This patch wont be needed in v35
+var MAX_GET_COLLECTION_BATCH_SIZE = 20;
+var _intervalId, _interval, _disabled, _addNft, _getNftState, _stopPolling, stopPolling_fn, _startPolling, startPolling_fn, _onPreferencesControllerStateChange, onPreferencesControllerStateChange_fn, _getOwnerNftApi, getOwnerNftApi_fn, _getOwnerNfts, getOwnerNfts_fn, _inProcessNftFetchingUpdates;
var NftDetectionController = class extends _pollingcontroller.StaticIntervalPollingController {
/**
* The controller options
-@@ -68,8 +73,10 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
+@@ -68,8 +71,10 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _disabled, void 0);
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _addNft, void 0);
_chunkZ4BLTVTBjs.__privateAdd.call(void 0, this, _getNftState, void 0);
@@ -663,19 +551,16 @@ index 5335fa5..ae37683 100644
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _getNftState, getNftState);
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _addNft, addNft);
this.messagingSystem.subscribe(
-@@ -126,62 +133,154 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
+@@ -126,62 +131,152 @@ var NftDetectionController = class extends _pollingcontroller.StaticIntervalPoll
*/
async detectNfts(options) {
const userAddress = options?.userAddress ?? this.messagingSystem.call("PreferencesController:getState").selectedAddress;
- if (!this.isMainnet() || _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _disabled)) {
-+ const { selectedNetworkClientId } = this.messagingSystem.call(
-+ "NetworkController:getState"
-+ );
-+ const {
-+ configuration: { chainId }
-+ } = this.messagingSystem.call(
-+ "NetworkController:getNetworkClientById",
-+ selectedNetworkClientId
++
++ const { selectedNetworkClientId } = this.messagingSystem.call("NetworkController:getState");
++ const { configuration: { chainId }} = this.messagingSystem.call(
++ "NetworkController:getNetworkClientById",
++ selectedNetworkClientId
+ );
+ if (!supportedNftDetectionNetworks.includes(chainId) || _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _disabled)) {
return;
@@ -727,19 +612,18 @@ index 5335fa5..ae37683 100644
+ const updateKey = `${chainId}:${userAddress}`;
+ if (updateKey in _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _inProcessNftFetchingUpdates)) {
+ await _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _inProcessNftFetchingUpdates)[updateKey];
-+ return;
++ return;
+ }
+ const {
-+ promise: inProgressUpdate,
-+ resolve: updateSucceeded,
-+ reject: updateFailed
-+ } = utils_1.createDeferredPromise.call(void 0, { suppressUnhandledRejection: true });
++ promise: inProgressUpdate,
++ resolve: updateSucceeded,
++ reject: updateFailed
++ } = utils_1.createDeferredPromise.call(void 0, { suppressUnhandledRejection: true });
+ _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _inProcessNftFetchingUpdates)[updateKey] = inProgressUpdate;
+ let next;
+ let apiNfts = [];
+ let resultNftApi;
-+
-+ try {
++ try{
+ do {
+ resultNftApi = await _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getOwnerNfts, getOwnerNfts_fn).call(this, userAddress, chainId, next);
+ apiNfts = resultNftApi.tokens.filter(
@@ -752,7 +636,7 @@ index 5335fa5..ae37683 100644
- networkClientId: options?.networkClientId
+ const collections = apiNfts.reduce((acc, currValue) => {
+ if (!acc.includes(currValue.token.contract) && currValue.token.contract === currValue?.token?.collection?.id) {
-+ acc.push(currValue.token.contract);
++ acc.push(currValue.token.contract);
+ }
+ return acc;
+ }, []);
@@ -766,108 +650,110 @@ index 5335fa5..ae37683 100644
+ );
+ params.append("chainId", "1");
+ const collectionResponseForBatch = await _controllerutils.fetchWithErrorHandling.call(void 0,
-+ {
-+ url: `${_controllerutils.NFT_API_BASE_URL}/collections?${params.toString()}`,
-+ options: {
++ {
++ url: `${_controllerutils.NFT_API_BASE_URL}/collections?${params.toString()}`,
++ options: {
+ headers: {
-+ Version: '1'
++ Version: '1'
+ }
-+ },
-+ timeout: 15000
-+ }
-+ );
-+ return {
++ },
++ timeout: 15000
++ }
++ );
++ return {
+ ...allResponses,
+ ...collectionResponseForBatch
-+ };
-+ },
++ };
++ },
+ initialResult: {}
+ });
++
+ if (collectionResponse.collections?.length) {
+ apiNfts.forEach((singleNFT) => {
-+ const found = collectionResponse.collections.find(
++ const found = collectionResponse.collections.find(
+ (elm) => elm.id?.toLowerCase() === singleNFT.token.contract.toLowerCase()
-+ );
-+ if (found) {
-+ singleNFT.token = {
-+ ...singleNFT.token,
-+ collection: {
-+ ...singleNFT.token.collection ?? {},
-+ creator: found?.creator,
-+ openseaVerificationStatus: found?.openseaVerificationStatus,
-+ contractDeployedAt: found.contractDeployedAt,
-+ ownerCount: found.ownerCount,
-+ topBid: found.topBid
-+ }
-+ };
-+ }
++ );
++ if (found) {
++ singleNFT.token = {
++ ...singleNFT.token,
++ collection: {
++ ...singleNFT.token.collection ?? {},
++ creator: found?.creator,
++ openseaVerificationStatus: found?.openseaVerificationStatus,
++ contractDeployedAt: found.contractDeployedAt,
++ ownerCount: found.ownerCount,
++ topBid: found.topBid
++ }
++ };
++ }
+ });
+ }
+ }
++
+ const addNftPromises = apiNfts.map(async (nft) => {
-+ const {
++ const {
+ tokenId,
+ contract,
+ kind,
+ image: imageUrl,
-+ imageSmall: imageThumbnailUrl,
-+ metadata: { imageOriginal: imageOriginalUrl } = {},
-+ name,
-+ description,
-+ attributes,
-+ topBid,
-+ lastSale,
-+ rarityRank,
-+ rarityScore,
-+ collection
-+ } = nft.token;
-+ let ignored;
-+ const { ignoredNfts } = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _getNftState).call(this);
-+ if (ignoredNfts.length) {
++ imageSmall: imageThumbnailUrl,
++ metadata: { imageOriginal: imageOriginalUrl } = {},
++ name,
++ description,
++ attributes,
++ topBid,
++ lastSale,
++ rarityRank,
++ rarityScore,
++ collection
++ } = nft.token;
++ let ignored;
++ const { ignoredNfts } = _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _getNftState).call(this);
++ if (ignoredNfts.length) {
+ ignored = ignoredNfts.find((c) => {
+ return c.address === _controllerutils.toChecksumHexAddress.call(void 0, contract) && c.tokenId === tokenId;
+ });
-+ }
-+ if (!ignored) {
-+ const nftMetadata = Object.assign(
-+ {},
-+ { name },
-+ description && { description },
-+ imageUrl && { image: imageUrl },
-+ imageThumbnailUrl && { imageThumbnail: imageThumbnailUrl },
-+ imageOriginalUrl && { imageOriginal: imageOriginalUrl },
-+ kind && { standard: kind.toUpperCase() },
-+ lastSale && { lastSale },
-+ attributes && { attributes },
-+ topBid && { topBid },
-+ rarityRank && { rarityRank },
-+ rarityScore && { rarityScore },
-+ collection && { collection }
-+ );
-+ await _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _addNft).call(this, contract, tokenId, {
-+ nftMetadata,
-+ userAddress,
-+ source: "detected" /* Detected */,
-+ networkClientId: options?.networkClientId
-+ });
-+ }
++ }
++ if (!ignored) {
++ const nftMetadata = Object.assign(
++ {},
++ { name },
++ description && { description },
++ imageUrl && { image: imageUrl },
++ imageThumbnailUrl && { imageThumbnail: imageThumbnailUrl },
++ imageOriginalUrl && { imageOriginal: imageOriginalUrl },
++ kind && { standard: kind.toUpperCase() },
++ lastSale && { lastSale },
++ attributes && { attributes },
++ topBid && { topBid },
++ rarityRank && { rarityRank },
++ rarityScore && { rarityScore },
++ collection && { collection }
++ );
++ await _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _addNft).call(this, contract, tokenId, {
++ nftMetadata,
++ userAddress,
++ source: "detected" /* Detected */,
++ networkClientId: options?.networkClientId
++ });
++ }
});
- }
- });
- await Promise.all(addNftPromises);
+ await Promise.all(addNftPromises);
-+ } while (next = resultNftApi.continuation);
++ } while(next = resultNftApi.continuation)
+ updateSucceeded();
-+ } catch (error) {
++ }catch(error){
+ updateFailed(error);
+ throw error;
-+ } finally {
++ }finally{
+ delete _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _inProcessNftFetchingUpdates)[updateKey];
+ }
}
};
_intervalId = new WeakMap();
-@@ -190,6 +289,7 @@ _disabled = new WeakMap();
+@@ -190,6 +285,7 @@ _disabled = new WeakMap();
_addNft = new WeakMap();
_getNftState = new WeakMap();
_stopPolling = new WeakSet();
@@ -875,7 +761,7 @@ index 5335fa5..ae37683 100644
stopPolling_fn = function() {
if (_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _intervalId)) {
clearInterval(_chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _intervalId));
-@@ -207,41 +307,26 @@ _onPreferencesControllerStateChange = new WeakSet();
+@@ -207,41 +303,26 @@ _onPreferencesControllerStateChange = new WeakSet();
onPreferencesControllerStateChange_fn = function({ useNftDetection }) {
if (!useNftDetection !== _chunkZ4BLTVTBjs.__privateGet.call(void 0, this, _disabled)) {
_chunkZ4BLTVTBjs.__privateSet.call(void 0, this, _disabled, !useNftDetection);
@@ -909,6 +795,13 @@ index 5335fa5..ae37683 100644
- });
- if (!nftApiResponse) {
- return nfts;
+- }
+- const newNfts = nftApiResponse.tokens?.filter(
+- (elm) => elm.token.isSpam === false && (elm.blockaidResult?.result_type ? elm.blockaidResult?.result_type === "Benign" /* Benign */ : true)
+- ) ?? [];
+- nfts = [...nfts, ...newNfts];
+- } while (next = nftApiResponse.continuation);
+- return nfts;
+getOwnerNfts_fn = async function(address, chainId, cursor) {
+ const convertedChainId = _controllerutils.convertHexToDecimal.call(void 0, chainId).toString();
+ const url = _chunkZ4BLTVTBjs.__privateMethod.call(void 0, this, _getOwnerNftApi, getOwnerNftApi_fn).call(this, {
@@ -917,15 +810,9 @@ index 5335fa5..ae37683 100644
+ next: cursor
+ });
+ const nftApiResponse = await _controllerutils.handleFetch.call(void 0, url, {
-+ headers: {
-+ Version: '1'
- }
-- const newNfts = nftApiResponse.tokens?.filter(
-- (elm) => elm.token.isSpam === false && (elm.blockaidResult?.result_type ? elm.blockaidResult?.result_type === "Benign" /* Benign */ : true)
-- ) ?? [];
-- nfts = [...nfts, ...newNfts];
-- } while (next = nftApiResponse.continuation);
-- return nfts;
++ headers: {
++ Version: '1'
++ }
+ });
+ return nftApiResponse;
};
@@ -949,14 +836,11 @@ index 6f461a4..a3573af 100644
}
});
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-UEDNQBJN.js b/node_modules/@metamask/assets-controllers/dist/chunk-UEDNQBJN.js
-index 80cecfb..f625ec2 100644
+index 80cecfb..e19a2e9 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-UEDNQBJN.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-UEDNQBJN.js
-@@ -378,9 +378,10 @@ fetchAndMapExchangeRatesForSupportedNativeCurrency_fn = async function({
- }
- return Object.entries(contractNativeInformations).reduce(
+@@ -380,7 +380,7 @@ fetchAndMapExchangeRatesForSupportedNativeCurrency_fn = async function({
(obj, [tokenAddress, token]) => {
-+ // This wont be needed in v33
obj = {
...obj,
- [tokenAddress.toLowerCase()]: { ...token }
@@ -964,7 +848,7 @@ index 80cecfb..f625ec2 100644
};
return obj;
},
-@@ -416,7 +417,7 @@ fetchAndMapExchangeRatesForUnsupportedNativeCurrency_fn = async function({
+@@ -416,7 +416,7 @@ fetchAndMapExchangeRatesForUnsupportedNativeCurrency_fn = async function({
...acc,
[tokenAddress]: {
...token,
@@ -974,10 +858,10 @@ index 80cecfb..f625ec2 100644
};
return acc;
diff --git a/node_modules/@metamask/assets-controllers/dist/chunk-Z6TBQQE5.js b/node_modules/@metamask/assets-controllers/dist/chunk-Z6TBQQE5.js
-index 2f1b66f..8436bd9 100644
+index 2f1b66f..f4acd79 100644
--- a/node_modules/@metamask/assets-controllers/dist/chunk-Z6TBQQE5.js
+++ b/node_modules/@metamask/assets-controllers/dist/chunk-Z6TBQQE5.js
-@@ -295,13 +295,11 @@ var CodefiTokenPricesServiceV2 = class {
+@@ -295,13 +295,12 @@ var CodefiTokenPricesServiceV2 = class {
(obj, tokenAddress) => {
const lowercasedTokenAddress = tokenAddress.toLowerCase();
const marketData = addressCryptoDataMap[lowercasedTokenAddress];
@@ -986,6 +870,7 @@ index 2f1b66f..8436bd9 100644
return obj;
}
- const { price } = marketData;
++
const token = {
tokenAddress,
- value: price,
@@ -1089,7 +974,7 @@ index 758a85e..a4a4b72 100644
bps?: number;
recipient?: string;
diff --git a/node_modules/@metamask/assets-controllers/dist/types/TokenBalancesController.d.ts b/node_modules/@metamask/assets-controllers/dist/types/TokenBalancesController.d.ts
-index 52bb3ac..b291078 100644
+index 52bb3ac..d4d5c0a 100644
--- a/node_modules/@metamask/assets-controllers/dist/types/TokenBalancesController.d.ts
+++ b/node_modules/@metamask/assets-controllers/dist/types/TokenBalancesController.d.ts
@@ -79,6 +79,11 @@ export declare class TokenBalancesController extends BaseController $IOS_ENV_FILE
echo "Build started..."
brew install watchman
@@ -380,22 +380,25 @@ buildIosQA(){
if [ ! -f "ios/release.xcconfig" ] ; then
echo "$IOS_ENV" | tr "|" "\n" > ios/release.xcconfig
fi
- ./node_modules/.bin/react-native run-ios --scheme MetaMask-QA--configuration Release --simulator "iPhone 13 Pro"
+ cd ios && xcodebuild -workspace MetaMask.xcworkspace -scheme MetaMask-QA -configuration Release -sdk iphonesimulator -derivedDataPath build
+ # ./node_modules/.bin/react-native run-ios --scheme MetaMask-QA- -configuration Release --simulator "iPhone 13 Pro"
fi
}
buildAndroidQA(){
+ echo "Start Android QA build..."
+
remapEnvVariableQA
- if [ "$PRE_RELEASE" = false ] ; then
- adb uninstall io.metamask.qa
- fi
+ # if [ "$PRE_RELEASE" = false ] ; then
+ # adb uninstall io.metamask.qa
+ # fi
prebuild_android
# Generate APK
- cd android && ./gradlew assembleQaRelease --no-daemon --max-workers 2
+ cd android && ./gradlew assembleQaRelease app:assembleQaReleaseAndroidTest -PminSdkVersion=26 -DtestBuildType=release
# GENERATE BUNDLE
if [ "$GENERATE_BUNDLE" = true ] ; then
@@ -407,9 +410,9 @@ buildAndroidQA(){
yarn build:android:checksum:qa
fi
- if [ "$PRE_RELEASE" = false ] ; then
- adb install app/build/outputs/apk/qa/release/app-qa-release.apk
- fi
+ # if [ "$PRE_RELEASE" = false ] ; then
+ # adb install app/build/outputs/apk/qa/release/app-qa-release.apk
+ # fi
}
buildAndroidRelease(){
diff --git a/wdio/step-definitions/common-steps.js b/wdio/step-definitions/common-steps.js
index 39f06270b6d..69da7223da4 100644
--- a/wdio/step-definitions/common-steps.js
+++ b/wdio/step-definitions/common-steps.js
@@ -54,7 +54,6 @@ Given(/^I have imported my wallet$/, async () => {
await MetaMetricsScreen.isScreenTitleVisible();
await MetaMetricsScreen.tapIAgreeButton();
await TermOfUseScreen.isDisplayed();
- await TermOfUseScreen.textIsDisplayed();
await TermOfUseScreen.tapAgreeCheckBox();
await TermOfUseScreen.tapScrollEndButton();
if (!(await TermOfUseScreen.isCheckBoxChecked())) {
diff --git a/yarn.lock b/yarn.lock
index ddce029d470..4d2b39ab497 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1288,16 +1288,15 @@
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0"
integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==
-"@consensys/on-ramp-sdk@1.28.3":
- version "1.28.3"
- resolved "https://registry.yarnpkg.com/@consensys/on-ramp-sdk/-/on-ramp-sdk-1.28.3.tgz#5c7c9293f6ee83e1a681a6a76f982e801d3e75ec"
- integrity sha512-QwWBFFqP3NMOhcJmcfmFd6fpEU3iXI/tYdWXM8u679/mY1/2rnRKUc3RnuWGFgs3qESZ5sUG2Xc7cp5OrmnDgA==
+"@consensys/on-ramp-sdk@1.28.5":
+ version "1.28.5"
+ resolved "https://registry.yarnpkg.com/@consensys/on-ramp-sdk/-/on-ramp-sdk-1.28.5.tgz#b9ff6c2b0f46abef30bd32a720f34d963aa76491"
+ integrity sha512-tBZ2ZsEz+du/vHF4NChTTvcdYddoJCtNZDZj5lry8/TeNxctnYiZGx2tHtw2GJHfYa9Uuux2hwtdhubcR//ZeA==
dependencies:
async "^3.2.3"
axios "^0.28.0"
axios-retry "^3.1.2"
crypto-js "^4.2.0"
- jsonpath-plus "^7.2.0"
reflect-metadata "^0.1.13"
uuid "^9.0.0"
@@ -4295,7 +4294,7 @@
"@metamask/superstruct" "^3.1.0"
"@metamask/utils" "^9.0.0"
-"@metamask/accounts-controller@^15.0.0", "@metamask/accounts-controller@^17.2.0", "@metamask/accounts-controller@^18.1.0", "@metamask/accounts-controller@^18.2.1":
+"@metamask/accounts-controller@^16.0.0", "@metamask/accounts-controller@^17.2.0", "@metamask/accounts-controller@^18.1.0", "@metamask/accounts-controller@^18.2.1":
version "18.2.1"
resolved "https://registry.yarnpkg.com/@metamask/accounts-controller/-/accounts-controller-18.2.1.tgz#8e4a842316e9b7bbd0409b36129f7123ba4a4c79"
integrity sha512-BEvux+ZFpTOQa6HbRl7i7Tq24ztqrZIsX+H0ePh47lU+N8RWq1q0JCItV+zbsgdcYnwhtcMZTsp4jJPQwPe2og==
@@ -4321,16 +4320,6 @@
"@metamask/controller-utils" "^11.3.0"
"@metamask/utils" "^9.1.0"
-"@metamask/approval-controller@^6.0.2":
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/@metamask/approval-controller/-/approval-controller-6.0.2.tgz#2bae3c293b73e8ecaf8cbba11fbc99c61adb4367"
- integrity sha512-G5FmEwRkKJPjCh3sA6g8eoYOcmLj/Y6Kllh663fYYbt1DZHHJtkrWBuaUnbTySS0VYqbTInpcgSRzuawE1VMgw==
- dependencies:
- "@metamask/base-controller" "^5.0.2"
- "@metamask/rpc-errors" "^6.2.1"
- "@metamask/utils" "^8.3.0"
- nanoid "^3.1.31"
-
"@metamask/approval-controller@^7.0.0", "@metamask/approval-controller@^7.0.1", "@metamask/approval-controller@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@metamask/approval-controller/-/approval-controller-7.0.2.tgz#bf46090fb900a5687b93978fb93eb3cf61d39a56"
@@ -4341,10 +4330,10 @@
"@metamask/utils" "^9.1.0"
nanoid "^3.1.31"
-"@metamask/assets-controllers@^31.0.0":
- version "31.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/assets-controllers/-/assets-controllers-31.0.0.tgz#c5db6df1903712a997a6c0b9e0a6f00a0f99def2"
- integrity sha512-AIdCI8QJDQU8b8prOUhVNh59YOh3HQ13wpXFMbgaYnY0BKZK0gzlSVRahK9ceEJYNNvT0eNM39fIV+rtjjaU7Q==
+"@metamask/assets-controllers@^32.0.0":
+ version "32.0.0"
+ resolved "https://registry.yarnpkg.com/@metamask/assets-controllers/-/assets-controllers-32.0.0.tgz#75175a848f0788036e2feff17e4bb245c2f7d223"
+ integrity sha512-d1+ASPADu+b3Jw77AxHX382F0VRxksGxDlJ/f0hzZDHkoNUqPVxX1wL3ZIyBQYh4je4Nq7MOslg5kwUPXF13rw==
dependencies:
"@ethereumjs/util" "^8.1.0"
"@ethersproject/address" "^5.7.0"
@@ -4352,17 +4341,17 @@
"@ethersproject/contracts" "^5.7.0"
"@ethersproject/providers" "^5.7.0"
"@metamask/abi-utils" "^2.0.2"
- "@metamask/accounts-controller" "^15.0.0"
- "@metamask/approval-controller" "^6.0.2"
- "@metamask/base-controller" "^5.0.2"
+ "@metamask/accounts-controller" "^16.0.0"
+ "@metamask/approval-controller" "^7.0.0"
+ "@metamask/base-controller" "^6.0.0"
"@metamask/contract-metadata" "^2.4.0"
- "@metamask/controller-utils" "^10.0.0"
+ "@metamask/controller-utils" "^11.0.0"
"@metamask/eth-query" "^4.0.0"
- "@metamask/keyring-controller" "^16.1.0"
+ "@metamask/keyring-controller" "^17.0.0"
"@metamask/metamask-eth-abis" "^3.1.1"
- "@metamask/network-controller" "^18.1.3"
- "@metamask/polling-controller" "^7.0.0"
- "@metamask/preferences-controller" "^12.0.0"
+ "@metamask/network-controller" "^19.0.0"
+ "@metamask/polling-controller" "^8.0.0"
+ "@metamask/preferences-controller" "^13.0.0"
"@metamask/rpc-errors" "^6.2.1"
"@metamask/utils" "^8.3.0"
"@types/bn.js" "^5.1.5"
@@ -4449,21 +4438,6 @@
resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-2.2.0.tgz#277764d0d56e37180ae7644a9d11eb96295b36fc"
integrity sha512-SM6A4C7vXNbVpgMTX67kfW8QWvu3eSXxMZlY5PqZBTkvri1s9zgQ0uwRkK5r2VXNEoVmXCDnnEX/tX5EzzgNUQ==
-"@metamask/controller-utils@^10.0.0":
- version "10.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-10.0.0.tgz#08f6576c1533ab919b1b15b640146e9598f732ea"
- integrity sha512-vO6lwIr3VSkkR/A9VCzxcpgLJhzgMvUvaAU9SF8ulXIhRIh3Eur4VDcXtcKNGNB8oTZcKbKJrsmAJCVfPZQ+zQ==
- dependencies:
- "@ethereumjs/util" "^8.1.0"
- "@metamask/eth-query" "^4.0.0"
- "@metamask/ethjs-unit" "^0.3.0"
- "@metamask/utils" "^8.3.0"
- "@spruceid/siwe-parser" "2.1.0"
- "@types/bn.js" "^5.1.5"
- bn.js "^5.2.1"
- eth-ens-namehash "^2.0.8"
- fast-deep-equal "^3.1.3"
-
"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.0.2", "@metamask/controller-utils@^11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.3.0.tgz#530fd22289f717b752b4a7b6e504e1f2911b30a4"
@@ -4544,7 +4518,7 @@
json-rpc-random-id "^1.0.1"
pify "^5.0.0"
-"@metamask/eth-hd-keyring@^7.0.1", "@metamask/eth-hd-keyring@^7.0.4":
+"@metamask/eth-hd-keyring@^7.0.4":
version "7.0.4"
resolved "https://registry.yarnpkg.com/@metamask/eth-hd-keyring/-/eth-hd-keyring-7.0.4.tgz#1db788db66d956b8334a1b174983b753e39701d6"
integrity sha512-+qXgo8hetQbRJb/j4poloBng1HxNMTQvZG2SsGmfXaauQrpIk7T7T/r3i8K0v1rKVwhqELooyRuB0QzWE1OrRQ==
@@ -4668,7 +4642,7 @@
ethereum-cryptography "^2.1.2"
tweetnacl "^1.0.3"
-"@metamask/eth-simple-keyring@^6.0.1", "@metamask/eth-simple-keyring@^6.0.5":
+"@metamask/eth-simple-keyring@^6.0.5":
version "6.0.5"
resolved "https://registry.yarnpkg.com/@metamask/eth-simple-keyring/-/eth-simple-keyring-6.0.5.tgz#3e2f783092f9d873740e43852880216192802290"
integrity sha512-VqpD/TnBMHyEC/16YNEcGF+A29tZ/bwMKZTjFvQTQbTjbDS9ext2zmZsO9KA6PBGqGkoIdAERuKoxedjcCWoYg==
@@ -4860,18 +4834,6 @@
"@noble/hashes" "^1.3.2"
"@scure/base" "^1.0.0"
-"@metamask/keyring-api@^6.1.1":
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/@metamask/keyring-api/-/keyring-api-6.4.0.tgz#8d8f0adfdddeac84ac55993c3ba424e2171d4bfe"
- integrity sha512-JGkW7WCQ+M9lQXl5nAe49AaNmbK8HE6x/RupFL8Z5o8E4IVIeQbqEv+lW/Y5RBq0ekToHrxehlR/jDvL1iT8pg==
- dependencies:
- "@metamask/snaps-sdk" "^4.2.0"
- "@metamask/utils" "^8.4.0"
- "@types/uuid" "^9.0.8"
- bech32 "^2.0.0"
- superstruct "^1.0.3"
- uuid "^9.0.1"
-
"@metamask/keyring-api@^8.1.0", "@metamask/keyring-api@^8.1.3":
version "8.1.3"
resolved "https://registry.yarnpkg.com/@metamask/keyring-api/-/keyring-api-8.1.3.tgz#53e6a68236b88592db5bd43cf7e0d7e97dfad818"
@@ -4884,26 +4846,7 @@
bech32 "^2.0.0"
uuid "^9.0.1"
-"@metamask/keyring-controller@^16.1.0":
- version "16.1.0"
- resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-16.1.0.tgz#f15e9a0fd0242dc049e964d0a72d0f2ce00d3463"
- integrity sha512-R0ceCSu8QmoY5HWvXw5VyyfzBwITQuRme4KqbYRuMEVhcJ2KqAhjZEmHnEfHCNpd7fu6O2vRPkqEIz/j4H6Gfw==
- dependencies:
- "@ethereumjs/util" "^8.1.0"
- "@keystonehq/metamask-airgapped-keyring" "^0.14.1"
- "@metamask/base-controller" "^5.0.2"
- "@metamask/browser-passworder" "^4.3.0"
- "@metamask/eth-hd-keyring" "^7.0.1"
- "@metamask/eth-sig-util" "^7.0.1"
- "@metamask/eth-simple-keyring" "^6.0.1"
- "@metamask/keyring-api" "^6.1.1"
- "@metamask/message-manager" "^9.0.0"
- "@metamask/utils" "^8.3.0"
- async-mutex "^0.5.0"
- ethereumjs-wallet "^1.0.1"
- immer "^9.0.6"
-
-"@metamask/keyring-controller@^17.2.1", "@metamask/keyring-controller@^17.2.2":
+"@metamask/keyring-controller@^17.0.0", "@metamask/keyring-controller@^17.2.1", "@metamask/keyring-controller@^17.2.2":
version "17.2.2"
resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-17.2.2.tgz#944bc693305b4a6e4f1e73739a82c4bc6573dd9a"
integrity sha512-Shqk0ybcTPrHQLlBJ1V+InuYC7nD3/a6Ws0XCcBCOfkLTXvtSooKIWBioK83XlHMHkfsM6+bySxSqXJVgJvBZw==
@@ -4944,19 +4887,6 @@
jsonschema "^1.2.4"
uuid "^8.3.2"
-"@metamask/message-manager@^9.0.0":
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/message-manager/-/message-manager-9.0.0.tgz#b6138321089e08e7df1c7d2c71b7531cfcc6623c"
- integrity sha512-G+iQXpROPF5Te7SsVz7fGJIx1PPb+Tn6oytGZgHBZavmG06kPpRwStCViJVl0g14pi4fkFoLgx3kdqgTQziErQ==
- dependencies:
- "@metamask/base-controller" "^5.0.2"
- "@metamask/controller-utils" "^10.0.0"
- "@metamask/eth-sig-util" "^7.0.1"
- "@metamask/utils" "^8.3.0"
- "@types/uuid" "^8.3.0"
- jsonschema "^1.2.4"
- uuid "^8.3.2"
-
"@metamask/message-signing-snap@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@metamask/message-signing-snap/-/message-signing-snap-0.3.3.tgz#0c37814baa7bb58c2b8bc84caa08b247ba78067b"
@@ -5000,26 +4930,6 @@
immer "^9.0.6"
uuid "^8.3.2"
-"@metamask/network-controller@^18.1.3":
- version "18.1.3"
- resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-18.1.3.tgz#0aa7dbaf06c7ccf1f381a151d13850eb32d197b0"
- integrity sha512-B79qGwhdNcmGtYOQWMZXKVSt88dowyP4Nf979QEX0opYe6Z4eZMZnGBezdl74cAcezEiDE1ro6X8UahB11IOTg==
- dependencies:
- "@metamask/base-controller" "^5.0.2"
- "@metamask/controller-utils" "^10.0.0"
- "@metamask/eth-block-tracker" "^9.0.2"
- "@metamask/eth-json-rpc-infura" "^9.1.0"
- "@metamask/eth-json-rpc-middleware" "^12.1.1"
- "@metamask/eth-json-rpc-provider" "^3.0.2"
- "@metamask/eth-query" "^4.0.0"
- "@metamask/json-rpc-engine" "^8.0.2"
- "@metamask/rpc-errors" "^6.2.1"
- "@metamask/swappable-obj-proxy" "^2.2.0"
- "@metamask/utils" "^8.3.0"
- async-mutex "^0.5.0"
- immer "^9.0.6"
- uuid "^8.3.2"
-
"@metamask/network-controller@^19.0.0":
version "19.0.0"
resolved "https://registry.yarnpkg.com/@metamask/network-controller/-/network-controller-19.0.0.tgz#83cb2e6f4f8f596126b47386e45d7cd5a349803d"
@@ -5211,19 +5121,6 @@
fast-json-stable-stringify "^2.1.0"
uuid "^8.3.2"
-"@metamask/polling-controller@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/polling-controller/-/polling-controller-7.0.0.tgz#5f3d44bd1daf039528ced8a935250aeb6936d0df"
- integrity sha512-Y46cDS2P+E8G8iotAULuuAGdz0pyk97ctaRwvs1MCKWxYWd/g5Vgrupl7R+nSjGAA+qhlZpte8VqGEIAo72Uxw==
- dependencies:
- "@metamask/base-controller" "^5.0.2"
- "@metamask/controller-utils" "^10.0.0"
- "@metamask/network-controller" "^18.1.3"
- "@metamask/utils" "^8.3.0"
- "@types/uuid" "^8.3.0"
- fast-json-stable-stringify "^2.1.0"
- uuid "^8.3.2"
-
"@metamask/polling-controller@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@metamask/polling-controller/-/polling-controller-8.0.0.tgz#4dedac3ce3821d9435562d3a43523d3161c8e089"
@@ -5268,13 +5165,13 @@
"@metamask/base-controller" "^5.0.2"
"@metamask/controller-utils" "^9.1.0"
-"@metamask/preferences-controller@^12.0.0":
- version "12.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/preferences-controller/-/preferences-controller-12.0.0.tgz#45b433a4066e85364d38cb673ade64c7ed6cd041"
- integrity sha512-jeIGuUCok+E3wL2n8G82lrxT31rHYCeQP78RbaxR1RXkzAgdIYjEnVSpxQocJgrSkuOhlHExl24g0qB/Pi3aXw==
+"@metamask/preferences-controller@^13.0.0":
+ version "13.0.3"
+ resolved "https://registry.yarnpkg.com/@metamask/preferences-controller/-/preferences-controller-13.0.3.tgz#b38dcda227436e935442989e2be5652316d29f8a"
+ integrity sha512-gNl+sTzvKFRj80h+fLNHGh72LCowBX1LvjZXzqHoTP4CZiHvMCO1vsQZY0Tboh7en9kwah1OdbQVzLRCgOsSIA==
dependencies:
- "@metamask/base-controller" "^5.0.2"
- "@metamask/controller-utils" "^10.0.0"
+ "@metamask/base-controller" "^7.0.1"
+ "@metamask/controller-utils" "^11.3.0"
"@metamask/profile-sync-controller@^0.9.7":
version "0.9.7"
@@ -5327,7 +5224,7 @@
readable-stream "^3.6.2"
webextension-polyfill "^0.10.0"
-"@metamask/providers@^17.0.0", "@metamask/providers@^17.1.2":
+"@metamask/providers@^17.1.2":
version "17.1.2"
resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-17.1.2.tgz#bb29c9cbf66be4c3f83d3e24ffea93f750b3db39"
integrity sha512-hACtF02yaUYThvWrRtVU4JAc+ZLCZ4PJUYBw6dK9Rze50J7zCxtss2mB7H8w76iLx//b5hjgXx6y92gPVjuYWg==
@@ -5566,18 +5463,6 @@
fast-xml-parser "^4.3.4"
superstruct "^1.0.3"
-"@metamask/snaps-sdk@^4.2.0":
- version "4.4.2"
- resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-4.4.2.tgz#6d7654ca3ecbcda5cd8689f49721c084241a4495"
- integrity sha512-V6d1kQdkCTYQ7Z3+ZVnMWjwsS2TRaKNnSRtSHgjATdSacW5d/1td2KbTs+1x1/cSe58ULKW1SBwRNy0i0c95hA==
- dependencies:
- "@metamask/key-tree" "^9.1.1"
- "@metamask/providers" "^17.0.0"
- "@metamask/rpc-errors" "^6.2.1"
- "@metamask/utils" "^8.3.0"
- fast-xml-parser "^4.3.4"
- superstruct "^1.0.3"
-
"@metamask/snaps-sdk@^6.0.0", "@metamask/snaps-sdk@^6.1.0", "@metamask/snaps-sdk@^6.4.0", "@metamask/snaps-sdk@^6.5.0", "@metamask/snaps-sdk@^6.5.1":
version "6.5.1"
resolved "https://registry.yarnpkg.com/@metamask/snaps-sdk/-/snaps-sdk-6.5.1.tgz#527691767d98c08c802656b020d5d94d6336623e"
@@ -5745,7 +5630,7 @@
semver "^7.3.8"
superstruct "^1.0.3"
-"@metamask/utils@^8.1.0", "@metamask/utils@^8.2.0", "@metamask/utils@^8.3.0", "@metamask/utils@^8.4.0":
+"@metamask/utils@^8.1.0", "@metamask/utils@^8.2.0", "@metamask/utils@^8.3.0":
version "8.4.0"
resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-8.4.0.tgz#f44812c96467a4e1b70b2edff6ee89a9caa4e354"
integrity sha512-dbIc3C7alOe0agCuBHM1h71UaEaEqOk2W8rAtEn8QGz4haH2Qq7MoK6i7v2guzvkJVVh79c+QCzIqphC3KvrJg==
@@ -6804,13 +6689,6 @@
prompts "^2.4.0"
semver "^7.5.2"
-"@react-native-community/datetimepicker@^7.5.0":
- version "7.7.0"
- resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-7.7.0.tgz#0d0162b0434c7b35883f8c5af846f35e23d045ec"
- integrity sha512-nYzZy4DQLRFUzKJShWzRleCaebmCJfZ1lIcFmZgMXJoiVuGJNw3OIGHSWmHhPETh3OhP1RO3to882d7WmDIyrA==
- dependencies:
- invariant "^2.2.4"
-
"@react-native-community/netinfo@^9.5.0":
version "9.5.0"
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-9.5.0.tgz#93663bbb105feb8f729b8f0271ee06ffc009f024"
@@ -12995,11 +12873,6 @@ async-mutex@^0.5.0:
dependencies:
tslib "^2.4.0"
-async@0.2.x, async@~0.2.9:
- version "0.2.10"
- resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
- integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
-
async@^1.4.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -14051,7 +13924,7 @@ cbor-sync@^1.0.4:
resolved "https://registry.yarnpkg.com/cbor-sync/-/cbor-sync-1.0.4.tgz#5a11a1ab75c2a14d1af1b237fd84aa8c1593662f"
integrity sha512-GWlXN4wiz0vdWWXBU71Dvc1q3aBo0HytqwAZnXF1wOwjqNnDWA1vZ1gDMFLlqohak31VQzmhiYfiCX5QSSfagA==
-chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -14563,11 +14436,6 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.20:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
-colors@0.6.x:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
- integrity sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=
-
colors@^1.1.2, colors@^1.3.3:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
@@ -15270,7 +15138,7 @@ csv-writer@^1.6.0:
resolved "https://registry.yarnpkg.com/csv-writer/-/csv-writer-1.6.0.tgz#d0cea44b6b4d7d3baa2ecc6f3f7209233514bcf9"
integrity sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==
-cycle@1.0.x, cycle@^1.0.3:
+cycle@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI=
@@ -15518,7 +15386,19 @@ dedent@^1.0.0:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a"
integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==
-deep-equal@*, deep-equal@^2.0.5:
+deep-equal@^1.0.0, deep-equal@^1.0.1, deep-equal@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
+ integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
+ dependencies:
+ is-arguments "^1.0.4"
+ is-date-object "^1.0.1"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object-keys "^1.1.1"
+ regexp.prototype.flags "^1.2.0"
+
+deep-equal@^2.0.5:
version "2.2.2"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.2.tgz#9b2635da569a13ba8e1cc159c2f744071b115daa"
integrity sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==
@@ -15542,18 +15422,6 @@ deep-equal@*, deep-equal@^2.0.5:
which-collection "^1.0.1"
which-typed-array "^1.1.9"
-deep-equal@^1.0.0, deep-equal@^1.0.1, deep-equal@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
- integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
- dependencies:
- is-arguments "^1.0.4"
- is-date-object "^1.0.1"
- is-regex "^1.0.4"
- object-is "^1.0.1"
- object-keys "^1.1.1"
- regexp.prototype.flags "^1.2.0"
-
deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -17925,11 +17793,6 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
-eyes@0.1.x:
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
- integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=
-
fake-merkle-patricia-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz#4b8c3acfb520afadf9860b1f14cd8ce3402cddd3"
@@ -19685,11 +19548,6 @@ i18n-js@3.0.11:
resolved "https://registry.yarnpkg.com/i18n-js/-/i18n-js-3.0.11.tgz#f9e96bdb641c5b9d6be12759d7c422089987ef02"
integrity sha512-v7dG3kYJTQTyox3NqDabPDE/ZotWntyMI9kh4cYi+XlCSnsIR+KBTS2opPyObL8WndnklcLzbNU92FP/mLge3Q==
-i@0.3.x:
- version "0.3.7"
- resolved "https://registry.yarnpkg.com/i/-/i-0.3.7.tgz#2a7437a923d59c14b17243dc63a549af24d85799"
- integrity sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==
-
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -20459,7 +20317,7 @@ isomorphic-ws@^4.0.1:
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
-isstream@0.1.x, isstream@~0.1.2:
+isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
@@ -22993,7 +22851,7 @@ mkdirp@0.5.1:
dependencies:
minimist "0.0.8"
-mkdirp@0.x.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.6, mkdirp@~0.5.1:
+mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.6, mkdirp@~0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
@@ -23226,7 +23084,7 @@ multiple-cucumber-html-reporter@^3.0.1:
open "^8.4.2"
uuid "^9.0.0"
-mute-stream@0.0.8, mute-stream@~0.0.4:
+mute-stream@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
@@ -23289,11 +23147,6 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
-ncp@0.4.x:
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/ncp/-/ncp-0.4.2.tgz#abcc6cbd3ec2ed2a729ff6e7c1fa8f01784a8574"
- integrity sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ=
-
ncp@2.0.0, ncp@^2.0.0, ncp@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
@@ -24599,16 +24452,6 @@ pkg-types@^1.0.3:
mlly "^1.2.0"
pathe "^1.1.0"
-pkginfo@0.3.x:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
- integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=
-
-pkginfo@0.x.x:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
- integrity sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=
-
please-upgrade-node@^3.1.1, please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
@@ -24966,17 +24809,6 @@ promise@^8.3.0:
dependencies:
asap "~2.0.6"
-prompt@^0.2.14:
- version "0.2.14"
- resolved "https://registry.yarnpkg.com/prompt/-/prompt-0.2.14.tgz#57754f64f543fd7b0845707c818ece618f05ffdc"
- integrity sha1-V3VPZPVD/XsIRXB8gY7OYY8F/9w=
- dependencies:
- pkginfo "0.x.x"
- read "1.0.x"
- revalidator "0.1.x"
- utile "0.2.x"
- winston "0.8.x"
-
prompts@^2.0.1, prompts@^2.4.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
@@ -25641,16 +25473,6 @@ react-native-camera@^3.36.0:
dependencies:
prop-types "^15.6.2"
-react-native-cli@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/react-native-cli/-/react-native-cli-2.0.1.tgz#f2cd3c7aa1b83828cdfba630e2dfd817df766d54"
- integrity sha1-8s08eqG4OCjN+6Yw4t/YF992bVQ=
- dependencies:
- chalk "^1.1.1"
- minimist "^1.2.0"
- prompt "^0.2.14"
- semver "^5.0.3"
-
react-native-confetti-cannon@^1.5.0:
version "1.5.2"
resolved "https://registry.yarnpkg.com/react-native-confetti-cannon/-/react-native-confetti-cannon-1.5.2.tgz#ca1a05edd2a64b080ea8b6238c49277908e1f123"
@@ -26428,13 +26250,6 @@ read-tls-client-hello@^1.0.0:
dependencies:
"@types/node" "*"
-read@1.0.x:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
- integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=
- dependencies:
- mute-stream "~0.0.4"
-
"readable-stream@2 || 3", readable-stream@3.6.2, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
@@ -26971,11 +26786,6 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-revalidator@0.1.x:
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b"
- integrity sha1-/s5hv6DBtSoga9axgZgYS91SOjs=
-
rfc4648@^1.0.0:
version "1.5.3"
resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.3.tgz#e62b81736c10361ca614efe618a566e93d0b41c0"
@@ -27001,13 +26811,6 @@ rgb2hex@^0.1.0:
resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.1.10.tgz#4fdd432665273e2d5900434940ceba0a04c8a8a8"
integrity sha512-vKz+kzolWbL3rke/xeTE2+6vHmZnNxGyDnaVW4OckntAIcc7DcZzWkQSfxMDwqHS8vhgySnIFyBUH7lIk6PxvQ==
-rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3:
- version "2.7.1"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
- integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
- dependencies:
- glob "^7.1.3"
-
rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -27015,6 +26818,13 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+ integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+ dependencies:
+ glob "^7.1.3"
+
rimraf@~2.4.0:
version "2.4.5"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
@@ -27300,7 +27110,7 @@ semver-compare@^1.0.0:
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
-"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^5.7.2, semver@~2.3.1:
+"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^5.7.2, semver@~2.3.1:
version "5.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
@@ -29496,18 +29306,6 @@ utila@~0.4:
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==
-utile@0.2.x:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/utile/-/utile-0.2.1.tgz#930c88e99098d6220834c356cbd9a770522d90d7"
- integrity sha1-kwyI6ZCY1iIINMNWy9mncFItkNc=
- dependencies:
- async "~0.2.9"
- deep-equal "*"
- i "0.3.x"
- mkdirp "0.x.x"
- ncp "0.4.x"
- rimraf "2.x.x"
-
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
@@ -30098,19 +29896,6 @@ winston-transport@^4.5.0:
readable-stream "^3.6.0"
triple-beam "^1.3.0"
-winston@0.8.x:
- version "0.8.3"
- resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0"
- integrity sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=
- dependencies:
- async "0.2.x"
- colors "0.6.x"
- cycle "1.0.x"
- eyes "0.1.x"
- isstream "0.1.x"
- pkginfo "0.3.x"
- stack-trace "0.0.x"
-
winston@3.x:
version "3.10.0"
resolved "https://registry.yarnpkg.com/winston/-/winston-3.10.0.tgz#d033cb7bd3ced026fed13bf9d92c55b903116803"