diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 36e2bc04bb1..df73bd5e8ad 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -6,6 +6,7 @@ ignores: - '@react-native-community/slider' - 'patch-package' - '@lavamoat/allow-scripts' + - '@lavamoat/git-safe-dependencies' - 'babel-plugin-inline-import' # This is used on the patch for TokenRatesController of Assets controllers, for we to be able to use the last version of it - cockatiel @@ -84,3 +85,10 @@ ignores: - 'app' - 'i18n-js' - 'images' + + ## Expo + - '@config-plugins/detox' + - 'cross-spawn' + - 'expo-build-properties' + - 'expo-dev-client' + diff --git a/.e2e.env.example b/.e2e.env.example index fd46ecba336..29071a958ef 100644 --- a/.e2e.env.example +++ b/.e2e.env.example @@ -3,3 +3,5 @@ export MM_TEST_ACCOUNT_SRP='word1 word... word12' export MM_TEST_ACCOUNT_ADDRESS='0x...' export MM_TEST_ACCOUNT_PRIVATE_KEY='' export IS_TEST="true" +# Temporary mechanism to enable security alerts API prior to release. +export MM_SECURITY_ALERTS_API_ENABLED="true" diff --git a/.eslintrc.js b/.eslintrc.js index 567466baec8..70565eb4945 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,7 +60,7 @@ module.exports = { }, }, { - files: ['scripts/**/*.js'], + files: ['scripts/**/*.js', 'app.config.js'], rules: { 'no-console': 0, 'import/no-commonjs': 0, @@ -76,6 +76,7 @@ module.exports = { { files: [ 'app/components/UI/Name/**/*.{js,ts,tsx}', + 'app/components/UI/SimulationDetails/**/*.{js,ts,tsx}', 'app/components/hooks/DisplayName/**/*.{js,ts,tsx}' ], rules: { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0b2a91a5098..1e6ddc635a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,54 +7,66 @@ app/component-library/ @MetaMask/design-system-engineers # Platform Team -patches/ @MetaMask/mobile-platform -app/core/Engine.ts @MetaMask/mobile-platform -app/core/Engine.test.js @MetaMask/mobile-platform -app/core/Analytics/ @MetaMask/mobile-platform -app/util/metrics/ @MetaMask/mobile-platform -app/components/hooks/useMetrics/ @MetaMask/mobile-platform - -# Supply Chain Team -bitrise.yml @MetaMask/supply-chain @MetaMask/mobile-platform -yarn.lock @MetaMask/supply-chain @MetaMask/mobile-platform -ios/Podfile.lock @MetaMask/supply-chain @MetaMask/mobile-platform +.github/CODEOWNERS @MetaMask/mobile-platform +patches/ @MetaMask/mobile-platform +app/core/Engine/Engine.ts @MetaMask/mobile-platform +app/core/Engine/Engine.test.ts @MetaMask/mobile-platform +app/core/Engine/index.ts @MetaMask/mobile-platform +app/core/Engine/types.ts @MetaMask/mobile-platform +app/core/Engine/controllers/RemoteFeatureFlagController/ @MetaMask/mobile-platform +app/core/Analytics/ @MetaMask/mobile-platform +app/util/metrics/ @MetaMask/mobile-platform +app/components/hooks/useMetrics/ @MetaMask/mobile-platform +app/selectors/featureFlagController/* @MetaMask/mobile-platform +app/selectors/featureFlagController/minimumAppVersion/ @MetaMask/mobile-platform +app/store/migrations/ @MetaMask/mobile-platform +bitrise.yml @MetaMask/mobile-platform +yarn.lock @MetaMask/mobile-platform +ios/Podfile.lock @MetaMask/mobile-platform # Ramps Team -app/components/UI/Ramp/ @MetaMask/ramp @MetaMask/mobile-platform -app/reducers/fiatOrders/ @MetaMask/ramp @MetaMask/mobile-platform +app/components/UI/Ramp/ @MetaMask/ramp +app/reducers/fiatOrders/ @MetaMask/ramp # Confirmation Team -app/components/Views/confirmations @MetaMask/confirmations @MetaMask/mobile-platform -ppom @MetaMask/confirmations @MetaMask/mobile-platform +app/components/Views/confirmations @MetaMask/confirmations +ppom @MetaMask/confirmations # All below files are maintained by the SDK team because they contain SDK related code, WalletConnect integrations, or critical SDK flows. -app/actions/sdk @MetaMask/sdk-devs @MetaMask/mobile-platform -app/components/Approvals/WalletConnectApproval @MetaMask/sdk-devs @MetaMask/mobile-platform -app/components/Views/SDK @MetaMask/sdk-devs @MetaMask/mobile-platform -app/components/Views/WalletConnectSessions @MetaMask/sdk-devs @MetaMask/mobile-platform -app/core/BackgroundBridge/WalletConnectPort.ts @MetaMask/sdk-devs @MetaMask/mobile-platform -app/core/DeeplinkManager @MetaMask/sdk-devs @MetaMask/mobile-platform -app/core/RPCMethods/RPCMethodMiddleware.ts @MetaMask/sdk-devs @MetaMask/mobile-platform -app/core/SDKConnect @MetaMask/sdk-devs @MetaMask/mobile-platform -app/core/WalletConnect @MetaMask/sdk-devs @MetaMask/mobile-platform -app/reducers/sdk @MetaMask/sdk-devs @MetaMask/mobile-platform -app/util/walletconnect.js @MetaMask/sdk-devs @MetaMask/mobile-platform +app/actions/sdk @MetaMask/sdk-devs +app/components/Approvals/WalletConnectApproval @MetaMask/sdk-devs +app/components/Views/SDK @MetaMask/sdk-devs +app/components/Views/WalletConnectSessions @MetaMask/sdk-devs +app/core/BackgroundBridge/WalletConnectPort.ts @MetaMask/sdk-devs +app/core/DeeplinkManager @MetaMask/sdk-devs +app/core/RPCMethods/RPCMethodMiddleware.ts @MetaMask/sdk-devs +app/core/SDKConnect @MetaMask/sdk-devs +app/core/WalletConnect @MetaMask/sdk-devs +app/reducers/sdk @MetaMask/sdk-devs +app/util/walletconnect.js @MetaMask/sdk-devs # Accounts Team -app/core/Encryptor/ @MetaMask/accounts-engineers +app/core/Encryptor/ @MetaMask/accounts-engineers +app/core/Engine/controllers/AccountsController @MetaMask/accounts-engineers # Swaps Team -app/components/UI/Swaps @MetaMask/swaps-engineers @MetaMask/mobile-platform +app/components/UI/Swaps @MetaMask/swaps-engineers # Notifications Team -app/components/Views/Notifications @MetaMask/notifications @MetaMask/mobile-platform -app/components/Views/Settings/NotificationsSettings @MetaMask/notifications @MetaMask/mobile-platform -app/components/UI/Notifications @MetaMask/notifications @MetaMask/mobile-platform -app/reducers/notification @MetaMask/notifications @MetaMask/mobile-platform -app/actions/notification @MetaMask/notifications @MetaMask/mobile-platform -app/selectors/notification @MetaMask/notifications @MetaMask/mobile-platform -app/util/notifications @MetaMask/notifications @MetaMask/mobile-platform -app/store/util/notifications @MetaMask/notifications @MetaMask/mobile-platform +app/components/Views/Notifications @MetaMask/notifications +app/components/Views/Settings/NotificationsSettings @MetaMask/notifications +app/components/UI/Notifications @MetaMask/notifications +app/reducers/notification @MetaMask/notifications +app/actions/notification @MetaMask/notifications +app/selectors/notification @MetaMask/notifications +app/util/notifications @MetaMask/notifications +app/store/util/notifications @MetaMask/notifications + +# Identity Team +app/actions/identity @MetaMask/identity +app/util/identity @MetaMask/identity +app/components/UI/ProfileSyncing @MetaMask/identity +e2e/specs/identity @MetaMask/identity # LavaMoat Team ses.cjs @MetaMask/supply-chain @@ -115,7 +127,6 @@ app/components/Views/QRAccountDisplay @MetaMask/wallet-ux app/components/Views/QRScanner @MetaMask/wallet-ux app/components/Views/Settings @MetaMask/wallet-ux app/components/Views/TermsAndConditions @MetaMask/wallet-ux - app/reducers/experimentalSettings @MetaMask/wallet-ux app/reducers/modals @MetaMask/wallet-ux app/reducers/navigation @MetaMask/wallet-ux diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 31ee91b2191..4214943e066 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -57,7 +57,8 @@ body: - In production (default) - In beta - During release testing - - On the development branch + - On main branch + - On a feature branch validations: required: true - type: input diff --git a/.github/guidelines/LABELING_GUIDELINES.md b/.github/guidelines/LABELING_GUIDELINES.md index ee49fad0639..4b36fc4275e 100644 --- a/.github/guidelines/LABELING_GUIDELINES.md +++ b/.github/guidelines/LABELING_GUIDELINES.md @@ -25,7 +25,8 @@ To merge your PR one of the following QA labels are required: - **Run E2E Smoke**: This label will kick-off E2E testing and trigger a check to make sure the E2E tests pass. ### Optional labels: -- **regression-develop**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on the development branch, i.e., `main`, but is not yet released in production. +- **regression-main**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on the development branch, i.e., `main`, but is not yet released in production. +- **feature-branch-bug**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on a feature branch, i.e., before merging to `main`. ### Labels prohibited when PR needs to be merged: Any PR that includes one of the following labels can not be merged: diff --git a/.github/scripts/bitrise/run-bitrise-e2e-check.ts b/.github/scripts/bitrise/run-bitrise-e2e-check.ts index f25b5a9e7ce..123920a036d 100644 --- a/.github/scripts/bitrise/run-bitrise-e2e-check.ts +++ b/.github/scripts/bitrise/run-bitrise-e2e-check.ts @@ -8,18 +8,142 @@ import { } from '../scripts.types'; import axios from 'axios'; +let octokitInstance: InstanceType | null = null; +let owner: string; +let repo: string; + main().catch((error: Error): void => { console.error(error); process.exit(1); }); + + +function getOctokitInstance(): InstanceType { + if (!octokitInstance) { + const githubToken = process.env.GITHUB_TOKEN; + if (!githubToken) { + throw new Error("GitHub token is not set in the environment variables"); + } + octokitInstance = getOctokit(githubToken); + } + return octokitInstance; +} + +async function upsertStatusCheck( + statusCheckName: string, + commitHash: string, + status: StatusCheckStatusType, + conclusion: CompletedConclusionType | undefined, + summary: string +): Promise { + const octokit = getOctokitInstance(); + + // List existing checks + const listResponse = await octokit.rest.checks.listForRef({ + owner, + repo, + ref: commitHash, + }); + + if (listResponse.status !== 200) { + core.setFailed( + `Failed to list checks for commit ${commitHash}, received status code ${listResponse.status}`, + ); + process.exit(1); + } + + const existingCheck = listResponse.data.check_runs.find(check => check.name === statusCheckName); + + if (existingCheck) { + console.log(`Check already exists: ${existingCheck.name}, updating...`); + // Update the existing check + const updateCheckResponse = await octokit.rest.checks.update({ + owner, + repo, + check_run_id: existingCheck.id, + name: statusCheckName, + status: status, + conclusion: conclusion, + output: { + title: `${statusCheckName} Status Check`, + summary: summary, + }, + }); + + if (updateCheckResponse.status !== 200) { + core.setFailed( + `Failed to update '${statusCheckName}' check with status ${status} for commit ${commitHash}, got status code ${updateCheckResponse.status}`, + ); + process.exit(1); + } + + console.log(`Updated existing check: ${statusCheckName} with id ${existingCheck.id} & status ${status} for commit ${commitHash}`); + + + + } else { + console.log(`Check does not exist: ${statusCheckName}, creating...`); + // Create a new status check + const createCheckResponse = await octokit.rest.checks.create({ + owner, + repo, + name: statusCheckName, + head_sha: commitHash, + status: status, + conclusion: conclusion, + started_at: new Date().toISOString(), + output: { + title: `${statusCheckName} Status Check`, + summary: summary, + }, + }); + + if (createCheckResponse.status !== 201) { + core.setFailed( + `Failed to create '${statusCheckName}' check with status ${status} for commit ${commitHash}, got status code ${createCheckResponse.status}`, + ); + process.exit(1); + } + + console.log(`Created check: ${statusCheckName} with id ${createCheckResponse.data.id} & status ${status} for commit ${commitHash}`); + } +} +// Determine whether E2E should run and provide the associated reason +function shouldRunBitriseE2E(antiLabel: boolean, hasSmokeTestLabel: boolean, isDocs: boolean, isFork: boolean, isMergeQueue: boolean): [boolean, string] { + + const conditions = [ + {condition: hasSmokeTestLabel, message: "The smoke test label is present.", shouldRun: true}, + {condition: isFork, message: "The pull request is from a fork.", shouldRun: false}, + {condition: isDocs, message: "The pull request is documentation related.", shouldRun: false}, + {condition: isMergeQueue, message: "The pull request is part of a merge queue.", shouldRun: false}, + {condition: antiLabel, message: "The pull request has the anti-label.", shouldRun: false} + ]; + + // Iterate through conditions to determine action + for (const {condition, message, shouldRun} of conditions) { + if (condition) { + return [shouldRun, message]; + } + } + + // Default case if no conditions met + return [false, "Unexpected scenario or no relevant labels found."]; +} + + async function main(): Promise { const githubToken = process.env.GITHUB_TOKEN; const e2eLabel = process.env.E2E_LABEL; + const antiLabel = process.env.NO_E2E_LABEL; const e2ePipeline = process.env.E2E_PIPELINE; const workflowName = process.env.WORKFLOW_NAME; const triggerAction = context.payload.action as PullRequestTriggerType; - const { owner, repo, number: pullRequestNumber } = context.issue; + // Assuming context.issue comes populated with owner and repo, as typical with GitHub Actions + const { owner: contextOwner, repo: contextRepo, number: pullRequestNumber } = context.issue; + owner = contextOwner; + repo = contextRepo; + const removeAndApplyInstructions = `Remove and re-apply the "${e2eLabel}" label to trigger a E2E smoke test on Bitrise.`; const mergeFromMainCommitMessagePrefix = `Merge branch 'main' into`; const pullRequestLink = `https://github.com/MetaMask/metamask-mobile/pull/${pullRequestNumber}`; @@ -42,7 +166,21 @@ async function main(): Promise { process.exit(1); } - const octokit: InstanceType = getOctokit(githubToken); + if (!antiLabel) { + core.setFailed('NO_E2E_LABEL not found'); + process.exit(1); + } + + // Logging for Pipeline debugging + console.log(`Trigger action: ${triggerAction}`); + console.log(`event: ${context.eventName}`); + console.log(`pullRequestNumber: ${pullRequestNumber}`); + + const mergeQueue = (context.eventName === 'merge_group') + const mqCommitHash = context.payload?.merge_group?.head_sha; + + + const octokit = getOctokitInstance(); const { data: prData } = await octokit.rest.pulls.get({ owner, @@ -51,42 +189,43 @@ async function main(): Promise { }); // Get the latest commit hash - const latestCommitHash = prData.head.sha; + const prCommitHash = prData?.head?.sha; + // Determine the latest commit hash depending if it's a PR or MQ + const latestCommitHash = mergeQueue ? mqCommitHash : prCommitHash; - // Check if the e2e smoke label is applied - const labels = prData.labels; + // Grab flags & labels + const labels = prData?.labels ?? []; const hasSmokeTestLabel = labels.some((label) => label.name === e2eLabel); + const hasAntiLabel = labels.some((label) => label.name === antiLabel); + const fork = context.payload.pull_request?.head.repo.fork || false; + const docs = mergeQueue ? false : prData.title.startsWith("docs:"); + + + console.log(`Docs: ${docs}`); + console.log(`Fork: ${fork}`); + console.log(`Merge Queue: ${mergeQueue}`); + console.log(`Has smoke test label: ${hasSmokeTestLabel}`); + console.log(`Anti label: ${hasAntiLabel}`); + + const [shouldRun, reason] = shouldRunBitriseE2E(hasAntiLabel, hasSmokeTestLabel, docs, fork, mergeQueue); + console.log(`Should run: ${shouldRun}, Reason: ${reason}`); + + // One of these two labels must exist for pull_request type + if (!mergeQueue && !hasSmokeTestLabel && !hasAntiLabel) { - // Pass check since e2e smoke label is not applied - if (!hasSmokeTestLabel) { + // Fail Status due to missing labels + await upsertStatusCheck(statusCheckName, latestCommitHash, StatusCheckStatusType.Completed, + CompletedConclusionType.Failure, `Failed due to missing labels. Please apply either ${e2eLabel} or ${antiLabel}.`); + return + } + + if (!shouldRun) { console.log( - `"${e2eLabel}" label not applied. Skipping Bitrise status check.`, + `Skipping Bitrise status check. due to the following reason: ${reason}`, ); - // Post success status (skipped) - const createStatusCheckResponse = await octokit.rest.checks.create({ - owner, - repo, - name: statusCheckName, - head_sha: latestCommitHash, - status: StatusCheckStatusType.Completed, - conclusion: CompletedConclusionType.Success, - started_at: new Date().toISOString(), - output: { - title: statusCheckTitle, - summary: 'Skip run since no E2E smoke label is applied', - }, - }); - if (createStatusCheckResponse.status === 201) { - console.log( - `Created '${statusCheckName}' check with skipped status for commit ${latestCommitHash}`, - ); - } else { - core.setFailed( - `Failed to create '${statusCheckName}' check with skipped status for commit ${latestCommitHash} with status code ${createStatusCheckResponse.status}`, - ); - process.exit(1); - } + await upsertStatusCheck(statusCheckName, latestCommitHash, StatusCheckStatusType.Completed, CompletedConclusionType.Success, + `Skip run since ${reason}`); return; } @@ -95,6 +234,8 @@ async function main(): Promise { triggerAction === PullRequestTriggerType.Labeled && context.payload?.label?.name === e2eLabel ) { + + console.log(`Starting Bitrise build for commit ${latestCommitHash}`); // Configure Bitrise configuration for API call const data = { build_params: { @@ -222,29 +363,11 @@ async function main(): Promise { } // Post pending status - const createStatusCheckResponse = await octokit.rest.checks.create({ - owner, - repo, - name: statusCheckName, - head_sha: latestCommitHash, - status: StatusCheckStatusType.InProgress, - started_at: new Date().toISOString(), - output: { - title: statusCheckTitle, - summary: `Test runs in progress... You can view them at ${buildLink}`, - }, - }); + console.log(`Posting pending status for commit ${latestCommitHash}`); + + await upsertStatusCheck( statusCheckName, latestCommitHash, StatusCheckStatusType.InProgress, undefined, `Test runs in progress... You can view them at ${buildLink}`); + - if (createStatusCheckResponse.status === 201) { - console.log( - `Created '${statusCheckName}' check for commit ${latestCommitHash}`, - ); - } else { - core.setFailed( - `Failed to create '${statusCheckName}' check for commit ${latestCommitHash} with status code ${createStatusCheckResponse.status}`, - ); - process.exit(1); - } return; } @@ -256,6 +379,8 @@ async function main(): Promise { const lastCommentPage = Math.ceil( numberOfTotalComments / numberOfCommentsToCheck, ); + + const { data: latestCommentBatch } = await octokit.rest.issues.listComments({ owner, repo, @@ -287,31 +412,13 @@ async function main(): Promise { // Bitrise comment doesn't exist, post fail status if (!bitriseComment) { - // Post fail status - const createStatusCheckResponse = await octokit.rest.checks.create({ - owner, - repo, - name: statusCheckName, - head_sha: latestCommitHash, - status: StatusCheckStatusType.Completed, - conclusion: CompletedConclusionType.Failure, - started_at: new Date().toISOString(), - output: { - title: statusCheckTitle, - summary: `No Bitrise comment found for commit ${latestCommitHash}. Try re-applying the '${e2eLabel}' label.`, - }, - }); - if (createStatusCheckResponse.status === 201) { - console.log( - `Created '${statusCheckName}' check for commit ${latestCommitHash}`, - ); - } else { - core.setFailed( - `Failed to create '${statusCheckName}' check for commit ${latestCommitHash} with status code ${createStatusCheckResponse.status}`, - ); - process.exit(1); - } + console.log(`Bitrise comment not detected for commit ${latestCommitHash}`); + + await upsertStatusCheck(statusCheckName, latestCommitHash, StatusCheckStatusType.Completed, + CompletedConclusionType.Failure, + `No Bitrise comment found for commit ${latestCommitHash}. Try re-applying the '${e2eLabel}' label.`); + return; } @@ -402,27 +509,6 @@ async function main(): Promise { } // Post status check - const createStatusCheckResponse = await octokit.rest.checks.create({ - owner, - repo, - name: statusCheckName, - head_sha: latestCommitHash, - started_at: new Date().toISOString(), - output: { - title: statusCheckTitle, - summary: statusMessage, - }, - ...checkStatus, - }); + await upsertStatusCheck(statusCheckName, latestCommitHash, checkStatus.status, checkStatus.conclusion, statusMessage); - if (createStatusCheckResponse.status === 201) { - console.log( - `Created '${statusCheckName}' check for commit ${latestCommitHash}`, - ); - } else { - core.setFailed( - `Failed to create '${statusCheckName}' check for commit ${latestCommitHash} with status code ${createStatusCheckResponse.status}`, - ); - process.exit(1); - } } diff --git a/.github/scripts/check-template-and-add-labels.ts b/.github/scripts/check-template-and-add-labels.ts index e0a59e21d8e..fef8a5585d1 100644 --- a/.github/scripts/check-template-and-add-labels.ts +++ b/.github/scripts/check-template-and-add-labels.ts @@ -20,7 +20,8 @@ import { TemplateType, templates } from './shared/template'; import { retrievePullRequest } from './shared/pull-request'; enum RegressionStage { - Development, + DevelopmentFeature, + DevelopmentMain, Testing, Beta, Production @@ -202,8 +203,10 @@ function extractRegressionStageFromBugReportIssueBody( const extractedAnswer = match ? match[1].trim() : undefined; switch (extractedAnswer) { - case 'On the development branch': - return RegressionStage.Development; + case 'On a feature branch': + return RegressionStage.DevelopmentFeature; + case 'On main branch': + return RegressionStage.DevelopmentMain; case 'During release testing': return RegressionStage.Testing; case 'In beta': @@ -317,11 +320,18 @@ async function userBelongsToMetaMaskOrg( // This function crafts appropriate label, corresponding to regression stage and release version. function craftRegressionLabel(regressionStage: RegressionStage | undefined, releaseVersion: string | undefined): Label { switch (regressionStage) { - case RegressionStage.Development: + case RegressionStage.DevelopmentFeature: + return { + name: `feature-branch-bug`, + color: '5319E7', // violet + description: `bug that was found on a feature branch, but not yet merged in main branch`, + }; + + case RegressionStage.DevelopmentMain: return { name: `regression-develop`, color: '5319E7', // violet - description: `Regression bug that was found on development branch, but not yet present in production`, + description: `Regression bug that was found on main branch, but not yet present in production`, }; case RegressionStage.Testing: diff --git a/.github/scripts/tsconfig.json b/.github/scripts/tsconfig.json index 4082f16a5d9..4bb976aefe1 100644 --- a/.github/scripts/tsconfig.json +++ b/.github/scripts/tsconfig.json @@ -1,3 +1,61 @@ { - "extends": "../../tsconfig.json" + "compilerOptions": { + /* Basic Options */ + "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "lib": [ + "es2017", + ] /* Specify library files to be included in the compilation. */, + "allowJs": true /* Allow javascript files to be compiled. */, + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "react-native" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true /* Do not emit outputs. */, + // "incremental": true, /* Enable incremental compilation */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "resolveJsonModule": true /* Allows importing JSON files */, + "baseUrl": "." /* Base directory to resolve non-absolute module names. */, + /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + "skipLibCheck": true /* Skip type checking of declaration files. */ + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "exclude": [ + "node_modules", + "jest.config.js" + ] } diff --git a/.github/workflows/auto-draft-prs.yml b/.github/workflows/auto-draft-prs.yml new file mode 100644 index 00000000000..994e6f0a488 --- /dev/null +++ b/.github/workflows/auto-draft-prs.yml @@ -0,0 +1,39 @@ +name: Auto Draft + +on: + pull_request: + types: [opened] + branches: + - main + +permissions: + pull-requests: write + contents: read + issues: write + +jobs: + process_pr: + runs-on: ubuntu-latest + steps: + - name: Convert PR to Draft and Add Label + uses: actions/github-script@v6 + with: + script: | + // Convert PR to draft + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + draft: true + }); + + // Check if the PR title includes "docs:" or if it's from a fork + if (context.payload.pull_request.title.includes('docs:') || context.payload.pull_request.head.repo.fork) { + // Add label "No E2E Smoke Needed" + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['No E2E Smoke Needed'] + }); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f13d4efd5a..71c11d98d64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,17 @@ jobs: echo "Duplicate dependencies detected; run 'yarn deduplicate' to remove them" exit 1 fi + git-safe-dependencies: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: yarn + - run: yarn setup --node + - name: Run @lavamoat/git-safe-dependencies + run: yarn git-safe-dependencies scripts: runs-on: ubuntu-20.04 strategy: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a95001cb1c6..6208030fb13 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -39,6 +39,6 @@ jobs: --rm \ -v "$(pwd):/app" -w /app \ metamask-mobile-builder:latest \ - bash -c 'yarn && yarn setup --build-ios' + bash -c 'yarn && yarn setup --build-ios --no-install-pods' # restore ownership for cleanup sudo chown -R "$(id -u):$(id -g)" . diff --git a/.github/workflows/run-bitrise-e2e-check.yml b/.github/workflows/run-bitrise-e2e-check.yml index ac1016e9d86..f30bc78962a 100644 --- a/.github/workflows/run-bitrise-e2e-check.yml +++ b/.github/workflows/run-bitrise-e2e-check.yml @@ -5,32 +5,18 @@ on: types: [edited, deleted] pull_request: types: [opened, reopened, labeled, unlabeled, synchronize] + merge_group: # Trigger on merge queue events to satisfy the branch protection rules + types: [checks_requested] env: E2E_LABEL: 'Run Smoke E2E' + NO_E2E_LABEL: 'No E2E Smoke Needed' E2E_PIPELINE: 'pr_smoke_e2e_pipeline' WORKFLOW_NAME: 'run-bitrise-e2e-check' jobs: - is-fork-pull-request: - name: Determine pull request source - if: ${{ github.event.issue.pull_request || github.event_name == 'pull_request' }} - runs-on: ubuntu-latest - outputs: - IS_FORK: ${{ steps.is-fork.outputs.IS_FORK }} - steps: - - uses: actions/checkout@v3 - - name: Determine whether this PR is from a fork - id: is-fork - run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${PR_NUMBER}" )" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.number || github.event.issue.number }} - run-bitrise-e2e-check: - needs: is-fork-pull-request runs-on: ubuntu-latest - if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} permissions: pull-requests: write contents: write diff --git a/.gitignore b/.gitignore index f25f95d5af6..092fc633fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ DerivedData project.xcworkspace ios/debug.xcconfig ios/release.xcconfig +.xcode.env.local # android / intellij build/ @@ -130,4 +131,8 @@ docs/assets/termsOfUse.html android/app/src/main/assets/modules.json # Google firebase base64 derived configs -**/GoogleService-Info.plist \ No newline at end of file +**/GoogleService-Info.plist +# Expo +.expo +dist/ +web-build/ diff --git a/.js.env.example b/.js.env.example index 1c11f591536..325ee52559d 100644 --- a/.js.env.example +++ b/.js.env.example @@ -1,16 +1,15 @@ # Sign up and generate your own keys at pubnub.com # Then rename this file to ".js.env" and rebuild the app -# +# # In order for this feature to work properly, you need to # build metamask-extension from source (https://github.com/MetaMask/metamask-extension) # and set your the same values there. -# +# # For more info take a look at https://github.com/MetaMask/metamask-extension/pull/5955 export MM_PUBNUB_SUB_KEY="" export MM_PUBNUB_PUB_KEY="" export MM_OPENSEA_KEY="" -export MM_ETHERSCAN_KEY="" export MM_FOX_CODE="EXAMPLE_FOX_CODE" # NOTE: Non-MetaMask only, will need to create an account and generate @@ -70,6 +69,10 @@ export SEGMENT_FLUSH_EVENT_LIMIT="1" # URL of security alerts API used to validate dApp requests. export SECURITY_ALERTS_API_URL="https://security-alerts.api.cx.metamask.io" +# Enable Portfolio View +export PORTFOLIO_VIEW="true" + + # Temporary mechanism to enable security alerts API prior to release. export MM_SECURITY_ALERTS_API_ENABLED="true" # Firebase @@ -81,7 +84,7 @@ export FCM_CONFIG_MESSAGING_SENDER_ID="" export FCM_CONFIG_APP_ID="" export GOOGLE_SERVICES_B64_ANDROID="" export GOOGLE_SERVICES_B64_IOS="" -#Notifications Feature Announcements +# Notifications Feature Announcements export FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN= export FEATURES_ANNOUNCEMENTS_SPACE_ID= @@ -94,10 +97,13 @@ export MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS="true" # Per dapp selected network (Amon Hen) feature flag export MM_PER_DAPP_SELECTED_NETWORK="" -export MM_CHAIN_PERMISSIONS="" +# Multichain permissions now set to true in production via the CI +# MM_MULTICHAIN_V1_ENABLED is the UI, and MM_CHAIN_PERMISSIONS is the engine +export MM_MULTICHAIN_V1_ENABLED="true" +export MM_CHAIN_PERMISSIONS="true" -#Multichain feature flag specific to UI changes +# Multichain feature flag specific to UI changes export MM_MULTICHAIN_V1_ENABLED="" -#Permissions Settings feature flag specific to UI changes +# Permissions Settings feature flag specific to UI changes export MM_PERMISSIONS_SETTINGS_V1_ENABLED="" diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e172ce58d..0cfee4c59bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,150 @@ ## Current Main Branch -## 7.35.0 - Nov 15, 2024 +## 7.37.1 - Dec 16, 2024 +### Fixed +- [#12577](https://github.com/MetaMask/metamask-mobile/pull/12577): chore: bump {gas-fee,network,selected-network,notification-services,profile-sync,signature}-controller (#12577) +- [#12694](https://github.com/MetaMask/metamask-mobile/pull/12694): fix: small refactoring of the latest migration script + add a new migration case (#12694) +- [#12664](https://github.com/MetaMask/metamask-mobile/pull/12664): fix: mark transactions as failed for cancelled / unknown smart transactions (#12664) + +## 7.37.0 - Nov 28, 2024 +### Added +- [#12091](https://github.com/MetaMask/metamask-mobile/pull/12091): feat: 2020 Add a performance test for iOS in Bitrise (#12091) +- [#12148](https://github.com/MetaMask/metamask-mobile/pull/12148): feat: Enable smart transactions for new users (#12148) +- [#12442](https://github.com/MetaMask/metamask-mobile/pull/12442): test: add a new unit test to cover for multichain feature flags ON (#12442) +- [#12420](https://github.com/MetaMask/metamask-mobile/pull/12420): feat(3598): non permitted chain flow small improvements (#12420) +- [#12198](https://github.com/MetaMask/metamask-mobile/pull/12198): feat: custom names for snap accounts (Flask only) (#12198) +- [#12396](https://github.com/MetaMask/metamask-mobile/pull/12396): feat(ramp): enable buy button in asset overview (#12396) +- [#11613](https://github.com/MetaMask/metamask-mobile/pull/11613): feat(ramp): improve amount editing formatting (#11613) +- [#12393](https://github.com/MetaMask/metamask-mobile/pull/12393): feat: Creating data tree for signed type V1 signatures (#12393) +- [#12160](https://github.com/MetaMask/metamask-mobile/pull/12160): feat: Integrate NFT api to display image & names in simulations includes `erc721`s (#12160) +- [#12324](https://github.com/MetaMask/metamask-mobile/pull/12324): feat: confirmation re-designs add basic page for types sign V1 signature request (#12324) +- [#11424](https://github.com/MetaMask/metamask-mobile/pull/11424): feat: add workflow for updating automated test results in TestRail (#11424) +- [#12337](https://github.com/MetaMask/metamask-mobile/pull/12337): feat: updated staking events to use withMetaMetrics helper (#12337) +- [#12363](https://github.com/MetaMask/metamask-mobile/pull/12363): feat: add PooledStaking slice for managing staking state (#12363) +- [#12398](https://github.com/MetaMask/metamask-mobile/pull/12398): feat: limit input digits to 12 in useInputHandler (#12398) +- [#12344](https://github.com/MetaMask/metamask-mobile/pull/12344): feat: upgrade assets controllers to v44 (#12344) +- [#12340](https://github.com/MetaMask/metamask-mobile/pull/12340): feat: upgrade assets controllers to version 43 (#12340) +- [#12270](https://github.com/MetaMask/metamask-mobile/pull/12270): feat: upgrade assets controllers to 42 with multichain token rates (#12270) +- [#12452](https://github.com/MetaMask/metamask-mobile/pull/12452): feat: updated staking events to use withMetaMetrics helper (#12337) (#12452) + +### Changed +- [#12356](https://github.com/MetaMask/metamask-mobile/pull/12356): chore: Remove unnecessary event prop (#12356) +- [#12425](https://github.com/MetaMask/metamask-mobile/pull/12425): ci: create ci workflow for multichain flow (#12425) +- [#12350](https://github.com/MetaMask/metamask-mobile/pull/12350): chore: Bump Snaps packages (#12350) +- [#11409](https://github.com/MetaMask/metamask-mobile/pull/11409): refactor: use `withKeyring` to batch account restore operation (#11409) +- [#12339](https://github.com/MetaMask/metamask-mobile/pull/12339): chore: Update accounts-controller @v19.0.0 and keyring-controller @v18.0.0 (#12339) +- [#12440](https://github.com/MetaMask/metamask-mobile/pull/12440): chore(ramp): upgrade sdk to 1.28.7 (#12440) +- [#12351](https://github.com/MetaMask/metamask-mobile/pull/12351): refactor(ramp): remove anonymous events (#12351) +- [#12355](https://github.com/MetaMask/metamask-mobile/pull/12355): chore: Add missing confirmation unit tests (#12355) +- [#12369](https://github.com/MetaMask/metamask-mobile/pull/12369): chore: upgrade transaction controller to increase polling rate (#12369) +- [#12202](https://github.com/MetaMask/metamask-mobile/pull/12202): refactor: update swaps quote poll count (#12202) +- [#10743](https://github.com/MetaMask/metamask-mobile/pull/10743): chore: @metamask/swaps-controller v9 -> v10 (#10743) +- [#12415](https://github.com/MetaMask/metamask-mobile/pull/12415): chore: Cherry pick 2506358 (merge in trackEvent work) (#12415) +- [#12238](https://github.com/MetaMask/metamask-mobile/pull/12238): chore: update codeowners (#12238) +- [#12416](https://github.com/MetaMask/metamask-mobile/pull/12416): chore: Chore/update accounts controller messenger code owner (#12416) +- [#12366](https://github.com/MetaMask/metamask-mobile/pull/12366): chore: #12184 MVP split engine file (#12366) +- [#12362](https://github.com/MetaMask/metamask-mobile/pull/12362): chore: Unit tests for tags approval controller undefined (#12362) +- [#12343](https://github.com/MetaMask/metamask-mobile/pull/12343): chore: Cherry pick f35d583 (#12343) +- [#12332](https://github.com/MetaMask/metamask-mobile/pull/12332): chore: do not show staked eth balance when balance is zero on homepage or asset detail (#12332) +- [#12413](https://github.com/MetaMask/metamask-mobile/pull/12413): chore: simplify cicd rls script (#12413) +- [#12334](https://github.com/MetaMask/metamask-mobile/pull/12334): chore: updating filter icon (#12334) + +### Fixed +- [#12313](https://github.com/MetaMask/metamask-mobile/pull/12313): fix: Remove run all tests section (#12313) +- [#12489](https://github.com/MetaMask/metamask-mobile/pull/12489): fix: replace end of navigation init and UIStartup span (#12489) +- [#12331](https://github.com/MetaMask/metamask-mobile/pull/12331): fix: tags pending approvals receiving undefined (#12331) +- [#10486](https://github.com/MetaMask/metamask-mobile/pull/10486): fix: limit ReactNativeWebview message size (#10486) +- [#12478](https://github.com/MetaMask/metamask-mobile/pull/12478): fix: incorrect event source in analytics and connection (#12478) +- [#10786](https://github.com/MetaMask/metamask-mobile/pull/10786): fix: added icon to walletconnect metadata (#10786) +- [#12455](https://github.com/MetaMask/metamask-mobile/pull/12455): fix: gas fee edit from swaps (#12455) +- [#12370](https://github.com/MetaMask/metamask-mobile/pull/12370): fix: Fix copy of ""Network fee"" on approval (#12370) +- [#12273](https://github.com/MetaMask/metamask-mobile/pull/12273): fix: Disable confirm button if `transactionMeta` is undefined (#12273) +- [#12367](https://github.com/MetaMask/metamask-mobile/pull/12367): fix: app crashing after send or swap (#12367) +- [#12446](https://github.com/MetaMask/metamask-mobile/pull/12446): fix: update wallet_addEthereumChain.js with correct MetricsEventBuilder (#12446) +- [#12180](https://github.com/MetaMask/metamask-mobile/pull/12180): fix: trackevent enabled is undefined (#12180) +- [#12315](https://github.com/MetaMask/metamask-mobile/pull/12315): fix: e2e: ensure Decrypt button is displayed (#12315) +- [#12402](https://github.com/MetaMask/metamask-mobile/pull/12402): fix: fix missing variable patch (#12402) +- [#12319](https://github.com/MetaMask/metamask-mobile/pull/12319): fix: hide rpc url selector for networks with one rpc (#12319) +- [#12371](https://github.com/MetaMask/metamask-mobile/pull/12371): fix: fix patch missing variable sentry error (#12371) +- [#12375](https://github.com/MetaMask/metamask-mobile/pull/12375): fix: breaking selector due to missing controller state (#12375) + +## 7.36.0 - Nov 15, 2024 +### Added +- [#12015](https://github.com/MetaMask/metamask-mobile/pull/12015): feat: 1957 crash screen redesign (#12015) +- [#12186](https://github.com/MetaMask/metamask-mobile/pull/12186): feat (cherry-pick): display staking transaction methods (#12110) (#12186) +- [#12110](https://github.com/MetaMask/metamask-mobile/pull/12110): feat: display staking transaction methods (#12110) +- [#12290](https://github.com/MetaMask/metamask-mobile/pull/12290): feat: STAKE-827: track additional pooled staking events (#12290) +- [#12280](https://github.com/MetaMask/metamask-mobile/pull/12280): feat: add loading skeleton for staking banners (#12280) +- [#12245](https://github.com/MetaMask/metamask-mobile/pull/12245): feat: add gas impact modal to stake confirmation input view (#12245) +- [#12263](https://github.com/MetaMask/metamask-mobile/pull/12263): feat: conditionally display stake/earn text based on pooled staking feature flag (#12261) (#12263) +- [#12146](https://github.com/MetaMask/metamask-mobile/pull/12146): feat: add staked ETH to metamask mobile homepage and account list menu (#12146) +- [#12261](https://github.com/MetaMask/metamask-mobile/pull/12261): feat: conditionally display stake/earn text based on pooled staking feature flag (#12261) +- [#12247](https://github.com/MetaMask/metamask-mobile/pull/12247): feat: update input colors and text formatting (#12247) +- [#12210](https://github.com/MetaMask/metamask-mobile/pull/12210): chore: disable pooled staking feature flag (#12210) +- [#12144](https://github.com/MetaMask/metamask-mobile/pull/12144): feat: add staking events (#12144) +- [#12268](https://github.com/MetaMask/metamask-mobile/pull/12268): feat: multichain currency rate polling (#12268) +- [#11808](https://github.com/MetaMask/metamask-mobile/pull/11808): feat: Token Network Filter UI [Mobile] (#11808) +- [#12171](https://github.com/MetaMask/metamask-mobile/pull/12171): feat: multichain polling hook (#12171) +- [#12168](https://github.com/MetaMask/metamask-mobile/pull/12168): feat(2808): improvements-and-small-features-and-small-fixes-that-still-needed-to-be-added-to-edit-permissions (#12168) +- [#11590](https://github.com/MetaMask/metamask-mobile/pull/11590): feat(2796): permission settings replace some of the mock data by real data (#11590) +- [#11511](https://github.com/MetaMask/metamask-mobile/pull/11511): feat: display snap name (#11511) +- [#12145](https://github.com/MetaMask/metamask-mobile/pull/12145): feat: disable wallet buttons for accounts that cannot sign transactions (#12145) +- [#12057](https://github.com/MetaMask/metamask-mobile/pull/12057): feat: team-label-token (#12057) +- [#11836](https://github.com/MetaMask/metamask-mobile/pull/11836): feat: upgrade @metamask/eth-ledger-bridge-keyring (#11836) + +### Changed +- [#11898](https://github.com/MetaMask/metamask-mobile/pull/11898): chore: New Crowdin translations by Github Action (#11898) +- [#12292](https://github.com/MetaMask/metamask-mobile/pull/12292): chore: Allow for higher versions of ruby (#12292) +- [#12291](https://github.com/MetaMask/metamask-mobile/pull/12291): chore: Remove notifications logic from wallet view (#12276) (#12291) +- [#12271](https://github.com/MetaMask/metamask-mobile/pull/12271): chore: Cache node installed via nvm on Bitrise (#12271) +- [#12121](https://github.com/MetaMask/metamask-mobile/pull/12121): chore: udpate LSMinimumSystemVersion (#12121) +- [#11658](https://github.com/MetaMask/metamask-mobile/pull/11658): chore: 8618 reduce enzyme usage in unit test by 25 (#11658) +- [#12257](https://github.com/MetaMask/metamask-mobile/pull/12257): refactor: remove global network usage from petnames (#12257) +- [#11996](https://github.com/MetaMask/metamask-mobile/pull/11996): chore: upgrade signature controller to remove global network (#11996) +- [#12274](https://github.com/MetaMask/metamask-mobile/pull/12274): chore: Update naming for returning a txHash asap for smart transactions (#12274) +- [#12287](https://github.com/MetaMask/metamask-mobile/pull/12287): docs: update onboarding readme (#12287) +- [#12234](https://github.com/MetaMask/metamask-mobile/pull/12234): chore: add unit test for native currency validation (#12234) +- [#12237](https://github.com/MetaMask/metamask-mobile/pull/12237): chore: Remove GoogleService files from git cache (#12237) +- [#12178](https://github.com/MetaMask/metamask-mobile/pull/12178): chore: upgrade assets-controllers to v41 (#12178) +- [#12209](https://github.com/MetaMask/metamask-mobile/pull/12209): chore: Modify gitignore to include generated ios/plist files (#12209) +- [#12286](https://github.com/MetaMask/metamask-mobile/pull/12286): chore: Add tags to UI Startup sentry transaction (#12286) +- [#12276](https://github.com/MetaMask/metamask-mobile/pull/12276): chore: Remove notifications logic from wallet view (#12276) +- [#12174](https://github.com/MetaMask/metamask-mobile/pull/12174): chore: Remove navigation instrumentation (#12174) +- [#12211](https://github.com/MetaMask/metamask-mobile/pull/12211): chore: disable pooled staking release for v7.35.0 (#12211) +- [#12194](https://github.com/MetaMask/metamask-mobile/pull/12194): chore: cicd error handling (#12194) +- [#12192](https://github.com/MetaMask/metamask-mobile/pull/12192): chore: fix release pr fixes (#12192) +- [#12175](https://github.com/MetaMask/metamask-mobile/pull/12175): chore: cicd - propagate changes to release pr from scripts (#12175) +- [#12225](https://github.com/MetaMask/metamask-mobile/pull/12225): chore: bump `@metamask/ppom-validator` to `0.35.1` (#12225) + +### Fixed +- [#12166](https://github.com/MetaMask/metamask-mobile/pull/12166): fix: remove SmokeNotifications tests for android on smoke tests pipeline (#12166) +- [#12217](https://github.com/MetaMask/metamask-mobile/pull/12217): fix: e2e: use different wallet SRP for non accounts tests (#12217) +- [#12197](https://github.com/MetaMask/metamask-mobile/pull/12197): fix: E2E: quarantine import-wallet-account tests (#12197) +- [#12250](https://github.com/MetaMask/metamask-mobile/pull/12250): fix: Add migration to fix NotificationServicesController bug (#12219) (#12250) +- [#12232](https://github.com/MetaMask/metamask-mobile/pull/12232): fix: e2e re-enable notifications android workflow (#12232) +- [#12219](https://github.com/MetaMask/metamask-mobile/pull/12219): fix: Add migration to fix NotificationServicesController bug (#12219) +- [#12120](https://github.com/MetaMask/metamask-mobile/pull/12120): fix: Onboarding failing biometrics locks screen for user instead of disabling biometrics and continuing with the onboarding (#12120) +- [#12177](https://github.com/MetaMask/metamask-mobile/pull/12177): fix: Create migration 59 to fix undefined selectedAccount (#12177) +- [#12311](https://github.com/MetaMask/metamask-mobile/pull/12311): fix: transaction reject crash (#12311) +- [#12228](https://github.com/MetaMask/metamask-mobile/pull/12228): fix: Update `transaction-controller` version (#12228) +- [#12100](https://github.com/MetaMask/metamask-mobile/pull/12100): fix: hide internal transaction origins in confirmation views (#12100) +- [#12283](https://github.com/MetaMask/metamask-mobile/pull/12283): fix: ensure unstake max will unstake all user shares (#12283) +- [#12231](https://github.com/MetaMask/metamask-mobile/pull/12231): fix: added ScrollView to stake confirmation review screen (#12231) +- [#12255](https://github.com/MetaMask/metamask-mobile/pull/12255): fix: fix displayed selected rpc for linea (#12255) +- [#11693](https://github.com/MetaMask/metamask-mobile/pull/11693): fix: relax network symbol length validation (#11693) +- [#12205](https://github.com/MetaMask/metamask-mobile/pull/12205): fix: add contractBalances as dependency (#12205) +- [#12235](https://github.com/MetaMask/metamask-mobile/pull/12235): fix: privacy mode is enabled in account selector by params (#12235) +- [#12282](https://github.com/MetaMask/metamask-mobile/pull/12282): fix: Lock ruby version to 3.1.6 and bump pod to 1.16.2 (#12282) + +## 7.35.1 - Nov 20, 2024 +### Fixed +- [#12331](https://github.com/MetaMask/metamask-mobile/pull/12331): fix: tags pending approvals receiving undefined (#12331) + +## 7.35.0 - Nov 4, 2024 ### Added - [#12078](https://github.com/MetaMask/metamask-mobile/pull/12078): chore(runway): cherry-pick feat: add favorites to browser menu (#12078) -- [#12159](https://github.com/MetaMask/metamask-mobile/pull/12159): feat: Add re-simulation feature (#12107) (#12159) +- [#12107](https://github.com/MetaMask/metamask-mobile/pull/12107): feat: Add re-simulation feature (#12107) - [#11770](https://github.com/MetaMask/metamask-mobile/pull/11770): feat: enable Security Alerts API (#11770) - [#11812](https://github.com/MetaMask/metamask-mobile/pull/11812): feat: network value component for re-designed confirmation pages (#11812) - [#11608](https://github.com/MetaMask/metamask-mobile/pull/11608): feat: enable sentry performance reporting on local development builds (#11608) diff --git a/README.md b/README.md index 92c2ef0d35a..2f47d76233d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ To learn how to contribute to the MetaMask codebase, visit our [Contributor Docs ## Documentation - [Architecture](./docs/readme/architecture.md) -- [Development Environment Setup](./docs/readme/environment.md) +- [Expo Development Environment Setup](./docs/readme/expo-environment.md) +- [Native Development Environment Setup](./docs/readme/environment.md) - [Build Troubleshooting](./docs/readme/troubleshooting.md) - [Testing](./docs/readme/testing.md) - [Debugging](./docs/readme/debugging.md) @@ -24,13 +25,67 @@ To learn how to contribute to the MetaMask codebase, visit our [Contributor Docs ## Getting started -### Environment setup +### Using Expo (recommended) -Before running the app, make sure your development environment has all the required tools. Several of these tools (ie Node and Ruby) may require specific versions in order to successfully build the app. +Expo is the fastest way to start developing. With the Expo framework, developers don't need to compile the native side of the application as before, hence no need for any native enviornment setup, developers only need to download a precompiled develpoment build and run the javascript bundler. The development build will then connect with the bundler to load the javascript code. + +#### Expo Environment Setup + +[Install node, yarn and watchman.](./docs/readme/expo-environment.md) + +#### Clone the project + +```bash +git clone git@github.com:MetaMask/metamask-mobile.git && \ +cd metamask-mobile +``` + +#### Install dependencies + +```bash +yarn setup:expo +``` + +#### Run the bundler + +```bash +yarn watch +``` + +#### Download and install the development build + +#### For internal developers +- Access Runway via Okta and go to the Expo bucket either on the iOS or Android section. From there you will see the available development builds (android-expo-dev-build.apk or ios-expo-dev-build.ipa). +- For Android: + - Install the .apk on your Android device or simulator. +- For iOS: + - Device: you need to have your iPhone registered with our Apple dev account. If you have it, you can install the .ipa on your device. + - Simulator: please follow the [native development section](https://github.com/MetaMask/metamask-mobile?tab=readme-ov-file#native-development) and run `yarn setup` and `yarn start:ios` as the .ipa will not work for now, we are working on having an .app that works on simulators. + +##### [SOON] For external developers (we are testing the new dev builds and will make them publicly available soon after) + +#### Load the app + +If on a simulator: +- use the initial expo screen that appears when starting the development to choose the bundler url +- OR press "a" for Android or "i" for iOS on the terminal where the bundler is running + +If on a physical device: +- Use the camera app to scan the QR code presented by the bundler running on the terminal + +That's it! This will work for any javascript development, if you need to develop or modify native code please see the next section. + +### Native Development + +If developing or modifying native code or installing any library that introduces or uses native code, it is not possible to use an Expo precompiled development build as you need to compile the native side of the application again. To do so, please follow the steps stated in this section. + +#### Native Environment setup + +Before running the app for native development, make sure your development environment has all the required tools. Several of these tools (ie Node and Ruby) may require specific versions in order to successfully build the app. [Setup your development environment](./docs/readme/environment.md) -### Building the app +#### Building the app **Clone the project** @@ -39,16 +94,16 @@ git clone git@github.com:MetaMask/metamask-mobile.git && \ cd metamask-mobile ``` -#### Firebase Messaging Setup +##### Firebase Messaging Setup MetaMask uses Firebase Cloud Messaging (FCM) to enable app communications. To integrate FCM, you’ll need configuration files for both iOS and Android platforms. -##### Internal Contributor instructions +###### Internal Contributor instructions 1. Grab the `.js.env` file from 1Password, ask around for the correct vault. This file contains the `GOOGLE_SERVICES_B64_ANDROID` and `GOOGLE_SERVICES_B64_IOS` secrets that will be used to generate the relevant configuration files for IOS/Android. 2. [Install](./README.md#install-dependencies) and [run & start](./README.md#running-the-app) the application as documented below. -##### External Contributor instructions +###### External Contributor instructions As an external contributor, you need to provide your own Firebase project configuration files: - **`GoogleService-Info.plist`** (iOS) @@ -77,7 +132,7 @@ export GOOGLE_SERVICES_B64_IOS="$(base64 -w0 -i ./ios/GoogleServices/GoogleServi In case of any doubt, please follow the instructions in the link below to get your Firebase project config file. [Firebase Project Quickstart](https://firebaseopensource.com/projects/firebase/quickstart-js/messaging/readme/#getting_started) -#### Install dependencies +##### Install dependencies ```bash yarn setup @@ -85,7 +140,7 @@ yarn setup _Not the usual install command, this will run scripts and a lengthy postinstall flow_ -### Running the app +#### Running the app for native development **Run Metro bundler** diff --git a/android/app/build.gradle b/android/app/build.gradle index 70cf56b3d51..5692a52de02 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,6 +47,11 @@ react { // // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" // hermesFlags = ["-O", "-output-source-map"] + // + // Added by install-expo-modules + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", rootDir.getAbsoluteFile().getParentFile().getAbsolutePath(), "android", "absolute"].execute(null, rootDir).text.trim()) + cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" } // Override default React Native to generate source maps for Hermes @@ -173,8 +178,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionName "7.35.0" - versionCode 1497 + versionName "7.37.1" + versionCode 1520 testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy 'react-native-camera', 'general' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bd03aa9f0cf..2770038a620 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -76,6 +76,7 @@ + @@ -114,6 +115,12 @@ + + + + + + diff --git a/android/app/src/main/java/io/metamask/MainActivity.java b/android/app/src/main/java/io/metamask/MainActivity.java index 1e1832d74cd..7bc4f9a2b65 100644 --- a/android/app/src/main/java/io/metamask/MainActivity.java +++ b/android/app/src/main/java/io/metamask/MainActivity.java @@ -1,4 +1,5 @@ package io.metamask; +import expo.modules.ReactActivityDelegateWrapper; import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; @@ -62,7 +63,7 @@ public void onNewIntent(Intent intent) { */ @Override protected ReactActivityDelegate createReactActivityDelegate() { - return new DefaultReactActivityDelegate(this, getMainComponentName(), DefaultNewArchitectureEntryPoint.getFabricEnabled()) { + return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate(this, getMainComponentName(), DefaultNewArchitectureEntryPoint.getFabricEnabled()) { @Override protected Bundle getLaunchOptions() { Bundle initialProperties = new Bundle(); @@ -73,6 +74,6 @@ protected Bundle getLaunchOptions() { } return initialProperties; } - }; + }); } } diff --git a/android/app/src/main/java/io/metamask/MainApplication.java b/android/app/src/main/java/io/metamask/MainApplication.java index 42be05c5b37..d3359c371db 100644 --- a/android/app/src/main/java/io/metamask/MainApplication.java +++ b/android/app/src/main/java/io/metamask/MainApplication.java @@ -1,4 +1,7 @@ package io.metamask; +import android.content.res.Configuration; +import expo.modules.ApplicationLifecycleDispatcher; +import expo.modules.ReactNativeHostWrapper; import android.app.Application; import com.facebook.react.ReactApplication; @@ -37,7 +40,7 @@ public String getFileProviderAuthority() { return BuildConfig.APPLICATION_ID + ".provider"; } - private final ReactNativeHost mReactNativeHost = new DefaultReactNativeHost(this) { + private final ReactNativeHost mReactNativeHost = new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) { @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; @@ -68,9 +71,9 @@ protected Boolean isHermesEnabled() { @Override protected String getJSMainModuleName() { - return "index"; + return ".expo/.virtual-metro-entry"; } - }; + }); @Override public ReactNativeHost getReactNativeHost() { @@ -112,5 +115,12 @@ public void onCreate() { } ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + ApplicationLifecycleDispatcher.onApplicationCreate(this); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); } } diff --git a/android/app/src/main/res/xml/react_native_config.xml b/android/app/src/main/res/xml/react_native_config.xml index 313da0eedad..aa2ca91bf79 100644 --- a/android/app/src/main/res/xml/react_native_config.xml +++ b/android/app/src/main/res/xml/react_native_config.xml @@ -6,5 +6,5 @@ 10.0.2.2 10.0.3.2 - + diff --git a/android/settings.gradle b/android/settings.gradle index 8e99b8ad9ae..1ba3878ea62 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -20,3 +20,6 @@ include ':react-native-gesture-handler' project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') include ':react-native-video' project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer') + +apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") +useExpoModules() \ No newline at end of file diff --git a/app.config.js b/app.config.js new file mode 100644 index 00000000000..f1f87b695a4 --- /dev/null +++ b/app.config.js @@ -0,0 +1,23 @@ +module.exports = { + name: 'MetaMask', + displayName: 'MetaMask', + plugins: [ + [ + 'expo-build-properties', + { + android: { + extraMavenRepos: [ + '../../node_modules/@notifee/react-native/android/libs' + ] + }, + ios: {} + } + ], + [ + '@config-plugins/detox', + { + subdomains: '*' + } + ] + ] +}; diff --git a/app.json b/app.json deleted file mode 100644 index 213fa9da6b0..00000000000 --- a/app.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "MetaMask", - "displayName": "MetaMask" -} diff --git a/app/actions/identity/constants/errors.ts b/app/actions/identity/constants/errors.ts new file mode 100644 index 00000000000..b4b7537dfad --- /dev/null +++ b/app/actions/identity/constants/errors.ts @@ -0,0 +1,8 @@ +export enum identityErrors { + PERFORM_SIGN_IN = 'Error while trying to sign in', + PERFORM_SIGN_OUT = 'Error while trying to sign out', + ENABLE_PROFILE_SYNCING = 'Error while trying to enable profile syncing', + DISABLE_PROFILE_SYNCING = 'Error while trying to disable profile syncing', +} + +export default identityErrors; diff --git a/app/actions/identity/index.test.ts b/app/actions/identity/index.test.ts new file mode 100644 index 00000000000..53bd04ac1bc --- /dev/null +++ b/app/actions/identity/index.test.ts @@ -0,0 +1,51 @@ +import { performSignIn, performSignOut } from '.'; +import Engine from '../../core/Engine'; + +jest.mock('../../core/Engine', () => ({ + resetState: jest.fn(), + context: { + AuthenticationController: { + performSignIn: jest.fn(), + performSignOut: jest.fn(), + getSessionProfile: jest.fn(), + }, + }, +})); + +describe('Identity actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('signs in successfully and obtain profile', async () => { + ( + Engine.context.AuthenticationController.performSignIn as jest.Mock + ).mockResolvedValue('valid-access-token'); + ( + Engine.context.AuthenticationController.getSessionProfile as jest.Mock + ).mockResolvedValue('valid-profile'); + + const result = await performSignIn(); + + expect( + Engine.context.AuthenticationController.performSignIn, + ).toHaveBeenCalled(); + expect( + Engine.context.AuthenticationController.getSessionProfile, + ).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('signs out successfully', async () => { + ( + Engine.context.AuthenticationController.performSignOut as jest.Mock + ).mockResolvedValue(undefined); + + const result = await performSignOut(); + + expect( + Engine.context.AuthenticationController.performSignOut, + ).toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); +}); diff --git a/app/actions/identity/index.ts b/app/actions/identity/index.ts new file mode 100644 index 00000000000..3a4605975ba --- /dev/null +++ b/app/actions/identity/index.ts @@ -0,0 +1,53 @@ +import { getErrorMessage } from '@metamask/utils'; +import Engine from '../../core/Engine'; +import identityErrors from './constants/errors'; + +export const performSignIn = async () => { + try { + const accessToken = + await Engine.context.AuthenticationController.performSignIn(); + if (!accessToken) { + return getErrorMessage(identityErrors.PERFORM_SIGN_IN); + } + + const profile = + await Engine.context.AuthenticationController.getSessionProfile(); + if (!profile) { + return getErrorMessage(identityErrors.PERFORM_SIGN_IN); + } + } catch (error) { + return getErrorMessage(error); + } +}; + +export const performSignOut = async () => { + try { + await Engine.context.AuthenticationController.performSignOut(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const enableProfileSyncing = async () => { + try { + await Engine.context.UserStorageController.enableProfileSyncing(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const disableProfileSyncing = async () => { + try { + await Engine.context.UserStorageController.disableProfileSyncing(); + } catch (error) { + return getErrorMessage(error); + } +}; + +export const syncInternalAccountsWithUserStorage = async () => { + try { + await Engine.context.UserStorageController.syncInternalAccountsWithUserStorage(); + } catch (error) { + return getErrorMessage(error); + } +}; diff --git a/app/actions/multichain/state.ts b/app/actions/multichain/state.ts new file mode 100644 index 00000000000..643638e0e91 --- /dev/null +++ b/app/actions/multichain/state.ts @@ -0,0 +1,4 @@ +export interface MultichainSettingsState { + bitcoinSupportEnabled: boolean; + bitcoinTestnetSupportEnabled: boolean; +} diff --git a/app/actions/navigation/index.ts b/app/actions/navigation/index.ts index 7a6fac7e9a0..b4c82b9a98e 100644 --- a/app/actions/navigation/index.ts +++ b/app/actions/navigation/index.ts @@ -1,18 +1,28 @@ /* eslint-disable import/prefer-default-export */ import { - SET_CURRENT_ROUTE, - SET_CURRENT_BOTTOM_NAV_ROUTE, -} from '../../reducers/navigation'; + type OnNavigationReadyAction, + type SetCurrentRouteAction, + type SetCurrentBottomNavRouteAction, + NavigationActionType, +} from './types'; -/** - * Action Creators - */ -export const setCurrentRoute = (route: string) => ({ - type: SET_CURRENT_ROUTE, +export * from './types'; + +export const setCurrentRoute = (route: string): SetCurrentRouteAction => ({ + type: NavigationActionType.SET_CURRENT_ROUTE, payload: { route }, }); -export const setCurrentBottomNavRoute = (route: string) => ({ - type: SET_CURRENT_BOTTOM_NAV_ROUTE, +export const setCurrentBottomNavRoute = ( + route: string, +): SetCurrentBottomNavRouteAction => ({ + type: NavigationActionType.SET_CURRENT_BOTTOM_NAV_ROUTE, payload: { route }, }); + +/** + * Action that is called when navigation is ready + */ +export const onNavigationReady = (): OnNavigationReadyAction => ({ + type: NavigationActionType.ON_NAVIGATION_READY, +}); diff --git a/app/actions/navigation/types.ts b/app/actions/navigation/types.ts new file mode 100644 index 00000000000..c57beba69da --- /dev/null +++ b/app/actions/navigation/types.ts @@ -0,0 +1,31 @@ +import { type Action } from 'redux'; + +/** + * Navigation action type enum + */ +export enum NavigationActionType { + ON_NAVIGATION_READY = 'ON_NAVIGATION_READY', + SET_CURRENT_ROUTE = 'SET_CURRENT_ROUTE', + SET_CURRENT_BOTTOM_NAV_ROUTE = 'SET_CURRENT_BOTTOM_NAV_ROUTE', +} + +export type OnNavigationReadyAction = + Action; + +export type SetCurrentRouteAction = + Action & { + payload: { route: string }; + }; + +export type SetCurrentBottomNavRouteAction = + Action & { + payload: { route: string }; + }; + +/** + * Navigation action + */ +export type NavigationAction = + | OnNavigationReadyAction + | SetCurrentRouteAction + | SetCurrentBottomNavRouteAction; diff --git a/app/actions/notification/constants/index.ts b/app/actions/notification/constants/index.ts index ee8c63fdffa..5c009930692 100644 --- a/app/actions/notification/constants/index.ts +++ b/app/actions/notification/constants/index.ts @@ -1,8 +1,4 @@ export enum notificationsErrors { - PERFORM_SIGN_IN = 'Error while trying to sign in', - PERFORM_SIGN_OUT = 'Error while trying to sign out', - ENABLE_PROFILE_SYNCING = 'Error while trying to enable profile syncing', - DISABLE_PROFILE_SYNCING = 'Error while trying to disable profile syncing', ENABLE_PUSH_NOTIFICATIONS = 'Error while trying to enable push notifications', DISABLE_PUSH_NOTIFICATIONS = 'Error while trying to disable push notifications', CHECK_ACCOUNTS_PRESENCE = 'Error while trying to check accounts presence', diff --git a/app/actions/notification/helpers/index.test.tsx b/app/actions/notification/helpers/index.test.tsx index 8fd7a7ea805..ddba30413d7 100644 --- a/app/actions/notification/helpers/index.test.tsx +++ b/app/actions/notification/helpers/index.test.tsx @@ -1,20 +1,15 @@ // Import necessary libraries and modules -import { signIn, signOut, enableNotificationServices, disableNotificationServices } from '.'; +import { enableNotificationServices, disableNotificationServices } from '.'; import Engine from '../../../core/Engine'; jest.mock('../../../core/Engine', () => ({ resetState: jest.fn(), context: { - AuthenticationController: { - performSignIn: jest.fn(), - performSignOut: jest.fn(), - getSessionProfile: jest.fn(), - }, NotificationServicesController: { - enableMetamaskNotifications:jest.fn(), - disableNotificationServices:jest.fn(), + enableMetamaskNotifications: jest.fn(), + disableNotificationServices: jest.fn(), checkAccountsPresence: jest.fn(), - } + }, }, })); @@ -23,41 +18,31 @@ describe('Notification Helpers', () => { jest.clearAllMocks(); }); - it('signs in successfully and obtain profile', async () => { - (Engine.context.AuthenticationController.performSignIn as jest.Mock).mockResolvedValue('valid-access-token'); - (Engine.context.AuthenticationController.getSessionProfile as jest.Mock).mockResolvedValue('valid-profile'); - - const result = await signIn(); - - expect(Engine.context.AuthenticationController.performSignIn).toHaveBeenCalled(); - expect(Engine.context.AuthenticationController.getSessionProfile).toHaveBeenCalled(); - expect(result).toBeUndefined(); - }); - - it('signs out successfully', async () => { - (Engine.context.AuthenticationController.performSignOut as jest.Mock).mockResolvedValue(undefined); - - const result = await signOut(); - - expect(Engine.context.AuthenticationController.performSignOut).toHaveBeenCalled(); - expect(result).toBeUndefined(); - }); - it('enables notification services successfully', async () => { - (Engine.context.NotificationServicesController.enableMetamaskNotifications as jest.Mock).mockResolvedValue(undefined); + ( + Engine.context.NotificationServicesController + .enableMetamaskNotifications as jest.Mock + ).mockResolvedValue(undefined); const result = await enableNotificationServices(); - expect(Engine.context.NotificationServicesController.enableMetamaskNotifications).toHaveBeenCalled(); + expect( + Engine.context.NotificationServicesController.enableMetamaskNotifications, + ).toHaveBeenCalled(); expect(result).toBeUndefined(); }); it('disables notification services successfully', async () => { - (Engine.context.NotificationServicesController.disableNotificationServices as jest.Mock).mockResolvedValue(undefined); + ( + Engine.context.NotificationServicesController + .disableNotificationServices as jest.Mock + ).mockResolvedValue(undefined); const result = await disableNotificationServices(); - expect(Engine.context.NotificationServicesController.disableNotificationServices).toHaveBeenCalled(); + expect( + Engine.context.NotificationServicesController.disableNotificationServices, + ).toHaveBeenCalled(); expect(result).toBeUndefined(); }); }); diff --git a/app/actions/notification/helpers/index.ts b/app/actions/notification/helpers/index.ts index a52e524009a..34b6b6ff064 100644 --- a/app/actions/notification/helpers/index.ts +++ b/app/actions/notification/helpers/index.ts @@ -2,56 +2,18 @@ import { getErrorMessage } from '@metamask/utils'; import { notificationsErrors } from '../constants'; import Engine from '../../../core/Engine'; -import { Notification, mmStorage, getAllUUIDs } from '../../../util/notifications'; -import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs'; +import { + Notification, + mmStorage, + getAllUUIDs, +} from '../../../util/notifications'; +import type { UserStorage } from '@metamask/notification-services-controller/notification-services'; export type MarkAsReadNotificationsParam = Pick< Notification, 'id' | 'type' | 'isRead' >[]; -export const signIn = async () => { - try { - const accessToken = - await Engine.context.AuthenticationController.performSignIn(); - if (!accessToken) { - return getErrorMessage(notificationsErrors.PERFORM_SIGN_IN); - } - - const profile = - await Engine.context.AuthenticationController.getSessionProfile(); - if (!profile) { - return getErrorMessage(notificationsErrors.PERFORM_SIGN_IN); - } - } catch (error) { - return getErrorMessage(error); - } -}; - -export const signOut = async () => { - try { - await Engine.context.AuthenticationController.performSignOut(); - } catch (error) { - return getErrorMessage(error); - } -}; - -export const enableProfileSyncing = async () => { - try { - await Engine.context.UserStorageController.enableProfileSyncing(); - } catch (error) { - return getErrorMessage(error); - } -}; - -export const disableProfileSyncing = async () => { - try { - await Engine.context.UserStorageController.disableProfileSyncing(); - } catch (error) { - return getErrorMessage(error); - } -}; - export const enableNotificationServices = async () => { try { await Engine.context.NotificationServicesController.enableMetamaskNotifications(); @@ -176,14 +138,6 @@ export const markMetamaskNotificationsAsRead = async ( } }; -export const syncInternalAccountsWithUserStorage = async () => { - try { - await Engine.context.UserStorageController.syncInternalAccountsWithUserStorage(); - } catch (error) { - return getErrorMessage(error); - } -}; - /** * Perform the deletion of the notifications storage key and the creation of on chain triggers to reset the notifications. * @@ -201,7 +155,11 @@ export const performDeleteStorage = async (): Promise => { return getErrorMessage(error); } }; -export const enablePushNotifications = async (userStorage: UserStorage, fcmToken?: string) => { + +export const enablePushNotifications = async ( + userStorage: UserStorage, + fcmToken?: string, +) => { try { const uuids = getAllUUIDs(userStorage); await Engine.context.NotificationServicesPushController.enablePushNotifications( @@ -224,7 +182,9 @@ export const disablePushNotifications = async (userStorage: UserStorage) => { } }; -export const updateTriggerPushNotifications = async (userStorage: UserStorage) => { +export const updateTriggerPushNotifications = async ( + userStorage: UserStorage, +) => { try { const uuids = getAllUUIDs(userStorage); await Engine.context.NotificationServicesPushController.updateTriggerPushNotifications( diff --git a/app/actions/onboarding/index.ts b/app/actions/onboarding/index.ts index 641bf5568bd..8b1e2c3e6e0 100644 --- a/app/actions/onboarding/index.ts +++ b/app/actions/onboarding/index.ts @@ -1,11 +1,11 @@ -import { IMetaMetricsEvent } from '../../core/Analytics'; +import { ITrackingEvent } from '../../core/Analytics/MetaMetrics.types'; export const SAVE_EVENT = 'SAVE_EVENT'; export const CLEAR_EVENTS = 'CLEAR_EVENTS'; interface SaveEventAction { type: typeof SAVE_EVENT; - event: [IMetaMetricsEvent]; + event: [ITrackingEvent]; } interface ClearEventsAction { @@ -15,7 +15,7 @@ interface ClearEventsAction { export type OnboardingActionTypes = SaveEventAction | ClearEventsAction; export function saveOnboardingEvent( - eventArgs: [IMetaMetricsEvent], + eventArgs: [ITrackingEvent], ): SaveEventAction { return { type: SAVE_EVENT, diff --git a/app/actions/user/index.js b/app/actions/user/index.js deleted file mode 100644 index fd996b8707f..00000000000 --- a/app/actions/user/index.js +++ /dev/null @@ -1,134 +0,0 @@ -// Constants -export const LOCKED_APP = 'LOCKED_APP'; -export const AUTH_SUCCESS = 'AUTH_SUCCESS'; -export const AUTH_ERROR = 'AUTH_ERROR'; -export const INTERRUPT_BIOMETRICS = 'INTERRUPT_BIOMETRICS'; -export const LOGIN = 'LOGIN'; -export const LOGOUT = 'LOGOUT'; - -export function interruptBiometrics() { - return { - type: INTERRUPT_BIOMETRICS, - }; -} - -export function lockApp() { - return { - type: LOCKED_APP, - }; -} - -export function authSuccess(bioStateMachineId) { - return { - type: AUTH_SUCCESS, - payload: { bioStateMachineId }, - }; -} - -export function authError(bioStateMachineId) { - return { - type: AUTH_ERROR, - payload: { bioStateMachineId }, - }; -} - -export function passwordSet() { - return { - type: 'PASSWORD_SET', - }; -} - -export function passwordUnset() { - return { - type: 'PASSWORD_UNSET', - }; -} - -export function seedphraseBackedUp() { - return { - type: 'SEEDPHRASE_BACKED_UP', - }; -} - -export function seedphraseNotBackedUp() { - return { - type: 'SEEDPHRASE_NOT_BACKED_UP', - }; -} - -export function backUpSeedphraseAlertVisible() { - return { - type: 'BACK_UP_SEEDPHRASE_VISIBLE', - }; -} - -export function backUpSeedphraseAlertNotVisible() { - return { - type: 'BACK_UP_SEEDPHRASE_NOT_VISIBLE', - }; -} - -export function protectWalletModalVisible() { - return { - type: 'PROTECT_MODAL_VISIBLE', - }; -} - -export function protectWalletModalNotVisible() { - return { - type: 'PROTECT_MODAL_NOT_VISIBLE', - }; -} - -export function loadingSet(loadingMsg) { - return { - type: 'LOADING_SET', - loadingMsg, - }; -} - -export function loadingUnset() { - return { - type: 'LOADING_UNSET', - }; -} - -export function setGasEducationCarouselSeen() { - return { - type: 'SET_GAS_EDUCATION_CAROUSEL_SEEN', - }; -} - -export function logIn() { - return { - type: LOGIN, - }; -} - -export function logOut() { - return { - type: LOGOUT, - }; -} - -export function setAppTheme(theme) { - return { - type: 'SET_APP_THEME', - payload: { theme }, - }; -} - -/** - * Temporary action to control auth flow - * - * @param {string} initialScreen - "login" or "onboarding" - * @returns - void - */ -export function checkedAuth(initialScreen) { - return { - type: 'CHECKED_AUTH', - payload: { - initialScreen, - }, - }; -} diff --git a/app/actions/user/index.ts b/app/actions/user/index.ts new file mode 100644 index 00000000000..9071fcffd50 --- /dev/null +++ b/app/actions/user/index.ts @@ -0,0 +1,161 @@ +import { type AppThemeKey } from '../../util/theme/models'; +import { + type InterruptBiometricsAction, + type LockAppAction, + type AuthSuccessAction, + type AuthErrorAction, + type PasswordSetAction, + type PasswordUnsetAction, + type SeedphraseBackedUpAction, + type SeedphraseNotBackedUpAction, + type BackUpSeedphraseVisibleAction, + type BackUpSeedphraseNotVisibleAction, + type ProtectModalVisibleAction, + type ProtectModalNotVisibleAction, + type LoadingSetAction, + type LoadingUnsetAction, + type SetGasEducationCarouselSeenAction, + type LoginAction, + type LogoutAction, + type SetAppThemeAction, + type CheckedAuthAction, + type PersistedDataLoadedAction, + UserActionType, +} from './types'; + +export * from './types'; + +export function interruptBiometrics(): InterruptBiometricsAction { + return { + type: UserActionType.INTERRUPT_BIOMETRICS, + }; +} + +export function lockApp(): LockAppAction { + return { + type: UserActionType.LOCKED_APP, + }; +} + +export function authSuccess(bioStateMachineId?: string): AuthSuccessAction { + return { + type: UserActionType.AUTH_SUCCESS, + payload: { bioStateMachineId }, + }; +} + +export function authError(bioStateMachineId?: string): AuthErrorAction { + return { + type: UserActionType.AUTH_ERROR, + payload: { bioStateMachineId }, + }; +} + +export function passwordSet(): PasswordSetAction { + return { + type: UserActionType.PASSWORD_SET, + }; +} + +export function passwordUnset(): PasswordUnsetAction { + return { + type: UserActionType.PASSWORD_UNSET, + }; +} + +export function seedphraseBackedUp(): SeedphraseBackedUpAction { + return { + type: UserActionType.SEEDPHRASE_BACKED_UP, + }; +} + +export function seedphraseNotBackedUp(): SeedphraseNotBackedUpAction { + return { + type: UserActionType.SEEDPHRASE_NOT_BACKED_UP, + }; +} + +export function backUpSeedphraseAlertVisible(): BackUpSeedphraseVisibleAction { + return { + type: UserActionType.BACK_UP_SEEDPHRASE_VISIBLE, + }; +} + +export function backUpSeedphraseAlertNotVisible(): BackUpSeedphraseNotVisibleAction { + return { + type: UserActionType.BACK_UP_SEEDPHRASE_NOT_VISIBLE, + }; +} + +export function protectWalletModalVisible(): ProtectModalVisibleAction { + return { + type: UserActionType.PROTECT_MODAL_VISIBLE, + }; +} + +export function protectWalletModalNotVisible(): ProtectModalNotVisibleAction { + return { + type: UserActionType.PROTECT_MODAL_NOT_VISIBLE, + }; +} + +export function loadingSet(loadingMsg: string): LoadingSetAction { + return { + type: UserActionType.LOADING_SET, + loadingMsg, + }; +} + +export function loadingUnset(): LoadingUnsetAction { + return { + type: UserActionType.LOADING_UNSET, + }; +} + +export function setGasEducationCarouselSeen(): SetGasEducationCarouselSeenAction { + return { + type: UserActionType.SET_GAS_EDUCATION_CAROUSEL_SEEN, + }; +} + +export function logIn(): LoginAction { + return { + type: UserActionType.LOGIN, + }; +} + +export function logOut(): LogoutAction { + return { + type: UserActionType.LOGOUT, + }; +} + +export function setAppTheme(theme: AppThemeKey): SetAppThemeAction { + return { + type: UserActionType.SET_APP_THEME, + payload: { theme }, + }; +} + +/** + * Temporary action to control auth flow + * + * @param initialScreen - "login" or "onboarding" + */ +export function checkedAuth(initialScreen: string): CheckedAuthAction { + return { + type: UserActionType.CHECKED_AUTH, + payload: { + initialScreen, + }, + }; +} + +/** + * Action to signal that persisted data has been loaded + */ +export function onPersistedDataLoaded(): PersistedDataLoadedAction { + return { + type: UserActionType.ON_PERSISTED_DATA_LOADED, + }; +} diff --git a/app/actions/user/types.ts b/app/actions/user/types.ts new file mode 100644 index 00000000000..704aee6092d --- /dev/null +++ b/app/actions/user/types.ts @@ -0,0 +1,111 @@ +import { type AppThemeKey } from '../../util/theme/models'; +import { type Action } from 'redux'; + +// Action type enum +export enum UserActionType { + LOCKED_APP = 'LOCKED_APP', + AUTH_SUCCESS = 'AUTH_SUCCESS', + AUTH_ERROR = 'AUTH_ERROR', + INTERRUPT_BIOMETRICS = 'INTERRUPT_BIOMETRICS', + LOGIN = 'LOGIN', + LOGOUT = 'LOGOUT', + ON_PERSISTED_DATA_LOADED = 'ON_PERSISTED_DATA_LOADED', + PASSWORD_SET = 'PASSWORD_SET', + PASSWORD_UNSET = 'PASSWORD_UNSET', + SEEDPHRASE_BACKED_UP = 'SEEDPHRASE_BACKED_UP', + SEEDPHRASE_NOT_BACKED_UP = 'SEEDPHRASE_NOT_BACKED_UP', + BACK_UP_SEEDPHRASE_VISIBLE = 'BACK_UP_SEEDPHRASE_VISIBLE', + BACK_UP_SEEDPHRASE_NOT_VISIBLE = 'BACK_UP_SEEDPHRASE_NOT_VISIBLE', + PROTECT_MODAL_VISIBLE = 'PROTECT_MODAL_VISIBLE', + PROTECT_MODAL_NOT_VISIBLE = 'PROTECT_MODAL_NOT_VISIBLE', + LOADING_SET = 'LOADING_SET', + LOADING_UNSET = 'LOADING_UNSET', + SET_GAS_EDUCATION_CAROUSEL_SEEN = 'SET_GAS_EDUCATION_CAROUSEL_SEEN', + SET_APP_THEME = 'SET_APP_THEME', + CHECKED_AUTH = 'CHECKED_AUTH', +} + +// User actions +export type LockAppAction = Action; + +export type AuthSuccessAction = Action & { + payload: { bioStateMachineId?: string }; +}; + +export type AuthErrorAction = Action & { + payload: { bioStateMachineId?: string }; +}; + +export type InterruptBiometricsAction = + Action; + +export type LoginAction = Action; + +export type LogoutAction = Action; + +export type PersistedDataLoadedAction = + Action; + +export type PasswordSetAction = Action; + +export type PasswordUnsetAction = Action; + +export type SeedphraseBackedUpAction = + Action; + +export type SeedphraseNotBackedUpAction = + Action; + +export type BackUpSeedphraseVisibleAction = + Action; + +export type BackUpSeedphraseNotVisibleAction = + Action; + +export type ProtectModalVisibleAction = + Action; + +export type ProtectModalNotVisibleAction = + Action; + +export type LoadingSetAction = Action & { + loadingMsg: string; +}; + +export type LoadingUnsetAction = Action; + +export type SetGasEducationCarouselSeenAction = + Action; + +export type SetAppThemeAction = Action & { + payload: { theme: AppThemeKey }; +}; + +export type CheckedAuthAction = Action & { + payload: { initialScreen: string }; +}; + +/** + * User actions union type + */ +export type UserAction = + | LockAppAction + | AuthSuccessAction + | AuthErrorAction + | InterruptBiometricsAction + | LoginAction + | LogoutAction + | PersistedDataLoadedAction + | PasswordSetAction + | PasswordUnsetAction + | SeedphraseBackedUpAction + | SeedphraseNotBackedUpAction + | BackUpSeedphraseVisibleAction + | BackUpSeedphraseNotVisibleAction + | ProtectModalVisibleAction + | ProtectModalNotVisibleAction + | LoadingSetAction + | LoadingUnsetAction + | SetGasEducationCarouselSeenAction + | SetAppThemeAction + | CheckedAuthAction; diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx index 280df813ba8..0e2a604be97 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react-native'; import CellSelectWithMenu from './CellSelectWithMenu'; -import { CellModalSelectorsIDs } from '../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../e2e/selectors/wallet/CellComponent.selectors'; import { SAMPLE_CELLSELECT_WITH_BUTTON_PROPS } from './CellSelectWithMenu.constants'; @@ -19,6 +19,6 @@ describe('CellSelectWithMenu', () => { , ); // Adjust the testID to match the one used in CellSelectWithMenu, if different - expect(queryByTestId(CellModalSelectorsIDs.MULTISELECT)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx index 144f2408e20..3327adff85b 100644 --- a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx @@ -11,7 +11,7 @@ import Tag from '../../../component-library/components/Tags/Tag'; // Internal dependencies. import styleSheet from './CellSelectWithMenu.styles'; import { CellSelectWithMenuProps } from './CellSelectWithMenu.types'; -import { CellModalSelectorsIDs } from '../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../e2e/selectors/wallet/CellComponent.selectors'; import ListItemMultiSelectButton from '../ListItemMultiSelectButton/ListItemMultiSelectButton'; import Avatar from '../../../component-library/components/Avatars/Avatar'; import Text from '../../../component-library/components/Texts/Text'; @@ -44,7 +44,7 @@ const CellSelectWithMenu = ({ @@ -52,7 +52,7 @@ const CellSelectWithMenu = ({ {withAvatar ? ( @@ -62,7 +62,7 @@ const CellSelectWithMenu = ({ {title} @@ -91,7 +91,7 @@ const CellSelectWithMenu = ({ )} {!!tagLabel && ( ({ + selectTokenMarketData: jest.fn(), +})); + +const mockSelectTokenMarketData = selectTokenMarketData as unknown as jest.Mock; + +jest.mock('../../../../selectors/currencyRateController', () => ({ + selectCurrentCurrency: jest.fn(() => 'USD'), +})); + +jest.mock('ethereumjs-util', () => ({ + toChecksumAddress: jest.fn((address) => address), + zeroAddress: jest.fn(() => '0x0000000000000000000000000000000000000000'), +})); + +describe('AggregatedPercentageCrossChains', () => { + const mockStore = configureStore([]); + let store: Store; + + beforeEach(() => { + store = mockStore({ + tokenRatesController: { + marketData: { + '1': { + '0xTokenAddress': { pricePercentChange1d: 5 }, + '0x0000000000000000000000000000000000000000': { + pricePercentChange1d: 3, + }, + }, + }, + }, + currencyRateController: { + currentCurrency: 'USD', + }, + }); + }); + + const testPositiveMarketData = { + '0x1': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: 0.26612196896783435, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + allTimeHigh: 0.00033787994095450243, + allTimeLow: 0.00024425950223297784, + circulatingSupply: 3590801926.36846, + currency: 'ETH', + dilutedMarketCap: 994523.5027585331, + high1d: 0.00027833552513055324, + id: 'dai', + low1d: 0.0002760612053968497, + marketCap: 994523.5027585331, + marketCapPercentChange1d: 1.94598, + price: 0.0002768602083719757, + priceChange1d: 0.00026184, + pricePercentChange14d: 0.06084239990548266, + pricePercentChange1d: 0.026199760027318986, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + allTimeHigh: 0.00032403240239079335, + allTimeLow: 0.00024306501355647231, + circulatingSupply: 39913626551.4682, + currency: 'ETH', + dilutedMarketCap: 11055935.156778876, + high1d: 0.0002780585743592791, + id: 'usd-coin', + low1d: 0.00027576902233315543, + marketCap: 11051818.92055066, + marketCapPercentChange1d: 0.05978, + price: 0.000276807033823891, + priceChange1d: -0.001160693663459944, + pricePercentChange14d: -0.05092221365479972, + pricePercentChange1d: 0.11599496417519209, + }, + }, + '0x89': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.9754341688014825, + allTimeLow: 0.43865817360906284, + circulatingSupply: 8081058250.420915, + currency: 'POL', + dilutedMarketCap: 10243600815.359032, + high1d: 0.9992817966819728, + id: 'polygon-ecosystem-token', + low1d: 0.8791508977097724, + marketCap: 8032270011.436278, + marketCapPercentChange1d: 6.17765, + price: 0.9962956752640171, + priceChange1d: 0.03843712, + pricePercentChange14d: 42.73351669766473, + pricePercentChange1d: 6.278884861668764, + }, + '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed': { + allTimeHigh: 1.88355350978746, + allTimeLow: 1.3144508332318727, + circulatingSupply: 0, + currency: 'POL', + dilutedMarketCap: 90736605.45696501, + high1d: 1.539001038484876, + id: 'axlusdc', + low1d: 1.522920391813105, + marketCap: 0, + marketCapPercentChange1d: 0, + price: 1.531344316900374, + priceChange1d: -0.00066261854319527, + pricePercentChange14d: 0.09924058418049628, + pricePercentChange1d: 0.0661881094544663, + }, + }, + '0xe708': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: 0.26612196896783435, + }, + }, + }; + + const testNegativeMarketData = { + '0x1': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: -0.26612196896783435, + }, + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + allTimeHigh: 0.00033787994095450243, + allTimeLow: 0.00024425950223297784, + circulatingSupply: 3590801926.36846, + currency: 'ETH', + dilutedMarketCap: 994523.5027585331, + high1d: 0.00027833552513055324, + id: 'dai', + low1d: 0.0002760612053968497, + marketCap: 994523.5027585331, + marketCapPercentChange1d: 1.94598, + price: 0.0002768602083719757, + priceChange1d: 0.00026184, + pricePercentChange14d: 0.06084239990548266, + pricePercentChange1d: -0.026199760027318986, + }, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': { + allTimeHigh: 0.00032403240239079335, + allTimeLow: 0.00024306501355647231, + circulatingSupply: 39913626551.4682, + currency: 'ETH', + dilutedMarketCap: 11055935.156778876, + high1d: 0.0002780585743592791, + id: 'usd-coin', + low1d: 0.00027576902233315543, + marketCap: 11051818.92055066, + marketCapPercentChange1d: 0.05978, + price: 0.000276807033823891, + priceChange1d: -0.001160693663459944, + pricePercentChange14d: -0.05092221365479972, + pricePercentChange1d: -0.11599496417519209, + }, + }, + '0x89': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.9754341688014825, + allTimeLow: 0.43865817360906284, + circulatingSupply: 8081058250.420915, + currency: 'POL', + dilutedMarketCap: 10243600815.359032, + high1d: 0.9992817966819728, + id: 'polygon-ecosystem-token', + low1d: 0.8791508977097724, + marketCap: 8032270011.436278, + marketCapPercentChange1d: 6.17765, + price: 0.9962956752640171, + priceChange1d: 0.03843712, + pricePercentChange14d: 42.73351669766473, + pricePercentChange1d: -6.278884861668764, + }, + '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed': { + allTimeHigh: 1.88355350978746, + allTimeLow: 1.3144508332318727, + circulatingSupply: 0, + currency: 'POL', + dilutedMarketCap: 90736605.45696501, + high1d: 1.539001038484876, + id: 'axlusdc', + low1d: 1.522920391813105, + marketCap: 0, + marketCapPercentChange1d: 0, + price: 1.531344316900374, + priceChange1d: -0.00066261854319527, + pricePercentChange14d: 0.09924058418049628, + pricePercentChange1d: -0.0661881094544663, + }, + }, + '0xe708': { + '0x0000000000000000000000000000000000000000': { + allTimeHigh: 1.3510378694759928, + allTimeLow: 0.0001199138679955242, + circulatingSupply: 120442102.974199, + currency: 'ETH', + dilutedMarketCap: 120513769.0674126, + high1d: 1.0196551936155827, + id: 'ethereum', + low1d: 0.9868614527890067, + marketCap: 120513769.0674126, + marketCapPercentChange1d: 0.43209, + price: 1.0000692350710725, + priceChange1d: 9.58, + pricePercentChange14d: 15.624001792435491, + pricePercentChange1d: -0.26612196896783435, + }, + }, + }; + + it('should match snapshot', () => { + const { toJSON } = render( + + + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('should return positive amount change if market data is all positive', () => { + mockSelectTokenMarketData.mockReturnValue(testPositiveMarketData); + const testTokenFiatBalancesCrossChains: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }[] = [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + balance: '3.08657', + tokenBalanceFiat: 3.11, + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + decimals: 18, + balance: '4.00229', + tokenBalanceFiat: 4.03, + }, + ], + tokenFiatBalances: [3.11, 4.03], + nativeFiatValue: 79.49, + }, + { + chainId: '0xe708', + tokensWithBalances: [], + tokenFiatBalances: [], + nativeFiatValue: 0, + }, + { + chainId: '0x89', + tokensWithBalances: [ + { + address: '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed', + symbol: 'AXLUSDC', + decimals: 6, + balance: '12.87735', + tokenBalanceFiat: 12.8, + }, + ], + tokenFiatBalances: [12.8], + nativeFiatValue: 9.28, + }, + ]; + const { getByTestId } = render( + + + , + ); + + const formattedValuePriceElement = getByTestId( + FORMATTED_VALUE_PRICE_TEST_ID, + ); + const formattedValuePercentageElement = getByTestId( + FORMATTED_PERCENTAGE_TEST_ID, + ); + + expect(formattedValuePriceElement).toBeDefined(); + expect(formattedValuePercentageElement).toBeDefined(); + expect(formattedValuePriceElement.props.children).toBe('+0.77 USD '); + expect(formattedValuePercentageElement.props.children).toBe('(+0.72%)'); + }); + + it('should return negative amount change if market data is all negative', () => { + mockSelectTokenMarketData.mockReturnValue(testNegativeMarketData); + const testTokenFiatBalancesCrossChains: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }[] = [ + { + chainId: '0x1', + tokensWithBalances: [ + { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + decimals: 6, + balance: '3.08657', + tokenBalanceFiat: 3.11, + }, + { + address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + symbol: 'DAI', + decimals: 18, + balance: '4.00229', + tokenBalanceFiat: 4.03, + }, + ], + tokenFiatBalances: [3.11, 4.03], + nativeFiatValue: 79.49, + }, + { + chainId: '0xe708', + tokensWithBalances: [], + tokenFiatBalances: [], + nativeFiatValue: 0, + }, + { + chainId: '0x89', + tokensWithBalances: [ + { + address: '0x750e4C4984a9e0f12978eA6742Bc1c5D248f40ed', + symbol: 'AXLUSDC', + decimals: 6, + balance: '12.87735', + tokenBalanceFiat: 12.8, + }, + ], + tokenFiatBalances: [12.8], + nativeFiatValue: 9.28, + }, + ]; + const { getByTestId } = render( + + + , + ); + + const formattedValuePriceElement = getByTestId( + FORMATTED_VALUE_PRICE_TEST_ID, + ); + const formattedValuePercentageElement = getByTestId( + FORMATTED_PERCENTAGE_TEST_ID, + ); + + expect(formattedValuePriceElement).toBeDefined(); + expect(formattedValuePercentageElement).toBeDefined(); + expect(formattedValuePriceElement.props.children).toBe('-0.85 USD '); + expect(formattedValuePercentageElement.props.children).toBe('(-0.77%)'); + }); +}); diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.tsx b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.tsx new file mode 100644 index 00000000000..8c6da659c23 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains.tsx @@ -0,0 +1,171 @@ +import React, { useMemo } from 'react'; +import { TextVariant } from '../../../../component-library/components/Texts/Text'; +import SensitiveText from '../../../../component-library/components/Texts/SensitiveText'; +import { View } from 'react-native'; +import { useSelector } from 'react-redux'; +import { selectCurrentCurrency } from '../../../../selectors/currencyRateController'; +import styleSheet from './AggregatedPercentage.styles'; +import { useStyles } from '../../../hooks'; +import { + FORMATTED_VALUE_PRICE_TEST_ID, + FORMATTED_PERCENTAGE_TEST_ID, +} from './AggregatedPercentage.constants'; +import { toChecksumAddress, zeroAddress } from 'ethereumjs-util'; +import { selectTokenMarketData } from '../../../../selectors/tokenRatesController'; +import { + MarketDataMapping, + TokensWithBalances, +} from '../../../../components/hooks/useGetFormattedTokensPerChain'; +import { getFormattedAmountChange, getPercentageTextColor } from './utils'; + +export interface AggregatedPercentageProps { + ethFiat: number; + tokenFiat: number; + tokenFiat1dAgo: number; + ethFiat1dAgo: number; +} + +export const getCalculatedTokenAmount1dAgo = ( + tokenFiatBalance: number, + tokenPricePercentChange1dAgo: number, +) => + tokenPricePercentChange1dAgo !== undefined && tokenFiatBalance + ? tokenFiatBalance / (1 + tokenPricePercentChange1dAgo / 100) + : tokenFiatBalance ?? 0; + +const isValidAmount = (amount: number | null | undefined): boolean => + amount !== null && amount !== undefined && !Number.isNaN(amount); + +const AggregatedPercentageCrossChains = ({ + privacyMode = false, + totalFiatCrossChains, + tokenFiatBalancesCrossChains, +}: { + privacyMode?: boolean; + totalFiatCrossChains: number; + tokenFiatBalancesCrossChains: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }[]; +}) => { + const crossChainMarketData: MarketDataMapping = useSelector( + selectTokenMarketData, + ); + + const totalFiat1dAgoCrossChains = useMemo(() => { + const getPerChainTotalFiat1dAgo = ( + chainId: string, + tokenFiatBalances: number[], + tokensWithBalances: TokensWithBalances[], + ) => { + const totalPerChain1dAgoERC20 = tokensWithBalances.reduce( + (total1dAgo: number, item: { address: string }, idx: number) => { + const found = + crossChainMarketData?.[chainId]?.[toChecksumAddress(item.address)]; + + const tokenFiat1dAgo = getCalculatedTokenAmount1dAgo( + tokenFiatBalances[idx], + found?.pricePercentChange1d, + ); + return total1dAgo + Number(tokenFiat1dAgo); + }, + 0, + ); + + return totalPerChain1dAgoERC20; + }; + return tokenFiatBalancesCrossChains.reduce( + ( + total1dAgoCrossChains: number, + item: { + chainId: string; + nativeFiatValue: number; + tokenFiatBalances: number[]; + tokensWithBalances: TokensWithBalances[]; + }, + ) => { + const perChainERC20Total = getPerChainTotalFiat1dAgo( + item.chainId, + item.tokenFiatBalances, + item.tokensWithBalances, + ); + + const nativePricePercentChange1d = + crossChainMarketData?.[item.chainId]?.[zeroAddress()] + ?.pricePercentChange1d; + + const nativeFiat1dAgo = getCalculatedTokenAmount1dAgo( + item.nativeFiatValue, + nativePricePercentChange1d, + ); + return ( + total1dAgoCrossChains + perChainERC20Total + Number(nativeFiat1dAgo) + ); + }, + 0, + ); + }, [tokenFiatBalancesCrossChains, crossChainMarketData]); + + const totalCrossChainBalance: number = Number(totalFiatCrossChains); + const crossChainTotalBalance1dAgo = totalFiat1dAgoCrossChains; + + const amountChangeCrossChains = + totalCrossChainBalance - crossChainTotalBalance1dAgo; + + const percentageChangeCrossChains = + (amountChangeCrossChains / crossChainTotalBalance1dAgo) * 100 || 0; + + const validFormattedPercentChange = `(${ + percentageChangeCrossChains >= 0 ? '+' : '' + }${percentageChangeCrossChains.toFixed(2)}%)`; + + const formattedPercentChangeCrossChains = isValidAmount( + percentageChangeCrossChains, + ) + ? validFormattedPercentChange + : ''; + const currentCurrency = useSelector(selectCurrentCurrency); + + const validFormattedAmountChange = getFormattedAmountChange( + amountChangeCrossChains, + currentCurrency, + ); + const formattedAmountChangeCrossChains = isValidAmount( + amountChangeCrossChains, + ) + ? validFormattedAmountChange + : ''; + + const percentageTextColor = getPercentageTextColor( + privacyMode, + percentageChangeCrossChains, + ); + const { styles } = useStyles(styleSheet, {}); + + return ( + + + {formattedAmountChangeCrossChains} + + + {formattedPercentChangeCrossChains} + + + ); +}; + +export default AggregatedPercentageCrossChains; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentageCrossChains.test.tsx.snap b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentageCrossChains.test.tsx.snap new file mode 100644 index 00000000000..bb203e9582e --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/__snapshots__/AggregatedPercentageCrossChains.test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggregatedPercentageCrossChains should match snapshot 1`] = ` + + + +0 USD + + + (+0.00%) + + +`; diff --git a/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts b/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts new file mode 100644 index 00000000000..77aea4b2ac2 --- /dev/null +++ b/app/component-library/components-temp/Price/AggregatedPercentage/utils.ts @@ -0,0 +1,32 @@ +import { DECIMALS_TO_SHOW } from '../../../../components/UI/Tokens/constants'; +import { renderFiat } from '../../../../util/number'; +import { TextColor } from '../../../components/Texts/Text'; + +export const getFormattedAmountChange = ( + input: number, + currentCurrency: string, +) => + `${input >= 0 ? '+' : ''}${renderFiat( + input, + currentCurrency, + DECIMALS_TO_SHOW, + )} `; + +export const getPercentageTextColor = ( + privacyMode: boolean, + percentageChangeCrossChains: number, +) => { + let percentageTextColor; + if (!privacyMode) { + if (percentageChangeCrossChains === 0) { + percentageTextColor = TextColor.Default; + } else if (percentageChangeCrossChains > 0) { + percentageTextColor = TextColor.Success; + } else { + percentageTextColor = TextColor.Error; + } + } else { + percentageTextColor = TextColor.Alternative; + } + return percentageTextColor; +}; diff --git a/app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap b/app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap index bda8377bdd9..03389274972 100644 --- a/app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap +++ b/app/component-library/components/BottomSheets/BottomSheetHeader/__snapshots__/BottomSheetHeader.test.tsx.snap @@ -4,7 +4,6 @@ exports[`BottomSheetHeader should render snapshot correctly 1`] = ` { , ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.DISPLAY)).not.toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.MULTISELECT)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.SELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.DISPLAY)).not.toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.SELECT)).toBe(null); }); it('should render CellMultiSelect given the type MultiSelect', () => { const wrapper = render( , ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.DISPLAY)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.MULTISELECT)).not.toBe( + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.DISPLAY)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).not.toBe( null, ); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.SELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.SELECT)).toBe(null); }); it('should render CellSelect given the type Select', () => { const wrapper = render( , ); expect(wrapper).toMatchSnapshot(); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.DISPLAY)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.MULTISELECT)).toBe(null); - expect(wrapper.queryByTestId(CellModalSelectorsIDs.SELECT)).not.toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.DISPLAY)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).toBe(null); + expect(wrapper.queryByTestId(CellComponentSelectorsIDs.SELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/Cell.tsx b/app/component-library/components/Cells/Cell/Cell.tsx index 354219dfb25..7ea7c925f89 100644 --- a/app/component-library/components/Cells/Cell/Cell.tsx +++ b/app/component-library/components/Cells/Cell/Cell.tsx @@ -6,7 +6,7 @@ import CellDisplay from './variants/CellDisplay'; import CellMultiSelect from './variants/CellMultiSelect'; import CellSelect from './variants/CellSelect'; import CellSelectWithMenu from '../../../components-temp/CellSelectWithMenu'; -import { CellModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import { CellProps, CellVariant } from './Cell.types'; @@ -14,20 +14,20 @@ import { CellProps, CellVariant } from './Cell.types'; const Cell = ({ variant, hitSlop, ...props }: CellProps) => { switch (variant) { case CellVariant.Display: - return ; + return ; case CellVariant.MultiSelect: return ( ); case CellVariant.Select: - return ; + return ; case CellVariant.SelectWithMenu: return ( ); diff --git a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx index 9be4ccb0612..ff18c2df818 100644 --- a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx +++ b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.test.tsx @@ -11,7 +11,7 @@ import { SAMPLE_CELLBASE_TERTIARY_TEXT, SAMPLE_CELLBASE_TAGLABEL, } from './CellBase.constants'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; describe('CellBase', () => { it('should render default settings correctly', () => { @@ -33,7 +33,7 @@ describe('CellBase', () => { title={SAMPLE_CELLBASE_TITLE} />, ); - expect(queryByTestId(CellModalSelectorsIDs.BASE_AVATAR)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.BASE_AVATAR)).not.toBe(null); }); it('should render the given title', async () => { diff --git a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx index 65c647c108d..8b48669c957 100644 --- a/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx +++ b/app/component-library/components/Cells/Cell/foundation/CellBase/CellBase.tsx @@ -19,7 +19,7 @@ import { } from './CellBase.constants'; import styleSheet from './CellBase.styles'; import { CellBaseProps } from './CellBase.types'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; const CellBase = ({ style, @@ -37,7 +37,7 @@ const CellBase = ({ {/* DEV Note: Account Avatar should be replaced with Avatar with Badge whenever available */} @@ -45,7 +45,7 @@ const CellBase = ({ {title} diff --git a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx index b0af59591dd..7addd5903ec 100644 --- a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { render } from '@testing-library/react-native'; //External dependencies -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import CellDisplay from './CellDisplay'; @@ -18,6 +18,6 @@ describe('CellDisplay', () => { const { queryByTestId } = render( , ); - expect(queryByTestId(CellModalSelectorsIDs.DISPLAY)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.DISPLAY)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx index f69a63194fb..6b0c12bd24b 100644 --- a/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellDisplay/CellDisplay.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { useStyles } from '../../../../../hooks'; import CellBase from '../../foundation/CellBase'; import Card from '../../../../Cards/Card'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import styleSheet from './CellDisplay.styles'; @@ -17,7 +17,7 @@ const CellDisplay = ({ style, ...props }: CellDisplayProps) => { const { styles } = useStyles(styleSheet, { style }); return ( - + ); diff --git a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx index df20af9fcde..ceb42effede 100644 --- a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.test.tsx @@ -5,7 +5,7 @@ import { render } from '@testing-library/react-native'; // Internal dependencies. import CellMultiSelect from './CellMultiSelect'; import { SAMPLE_CELLMULTISELECT_PROPS } from './CellMultiSelect.constants'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; describe('CellMultiSelect', () => { it('should render default settings correctly', () => { @@ -18,6 +18,6 @@ describe('CellMultiSelect', () => { const { queryByTestId } = render( , ); - expect(queryByTestId(CellModalSelectorsIDs.MULTISELECT)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.MULTISELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx index 3870d5fda28..c129e03884c 100644 --- a/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellMultiSelect/CellMultiSelect.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { useStyles } from '../../../../../hooks'; import ListItemMultiSelect from '../../../../List/ListItemMultiSelect'; import CellBase from '../../foundation/CellBase'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; // Internal dependencies. import styleSheet from './CellMultiSelect.styles'; @@ -30,7 +30,7 @@ const CellMultiSelect = ({ { it('should render default settings correctly', () => { @@ -16,6 +16,6 @@ describe('CellSelect', () => { const { queryByTestId } = render( , ); - expect(queryByTestId(CellModalSelectorsIDs.SELECT)).not.toBe(null); + expect(queryByTestId(CellComponentSelectorsIDs.SELECT)).not.toBe(null); }); }); diff --git a/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx b/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx index 1b9e4b7e583..d131c40ade6 100644 --- a/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx +++ b/app/component-library/components/Cells/Cell/variants/CellSelect/CellSelect.tsx @@ -11,7 +11,7 @@ import CellBase from '../../foundation/CellBase'; // Internal dependencies. import styleSheet from './CellSelect.styles'; import { CellSelectProps } from './CellSelect.types'; -import { CellModalSelectorsIDs } from '../../../../../../../e2e/selectors/Modals/CellModal.selectors'; +import { CellComponentSelectorsIDs } from '../../../../../../../e2e/selectors/wallet/CellComponent.selectors'; const CellSelect = ({ style, @@ -30,7 +30,7 @@ const CellSelect = ({ { const { colors } = useTheme(); diff --git a/app/component-library/components/Navigation/TabBar/TabBar.tsx b/app/component-library/components/Navigation/TabBar/TabBar.tsx index 9ec1e39f408..cd0eee06dd6 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.tsx +++ b/app/component-library/components/Navigation/TabBar/TabBar.tsx @@ -25,7 +25,7 @@ import OnboardingWizard from '../../../../components/UI/OnboardingWizard'; const TabBar = ({ state, descriptors, navigation }: TabBarProps) => { const { colors } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const { bottom: bottomInset } = useSafeAreaInsets(); const { styles } = useStyles(styleSheet, { bottomInset }); const chainId = useSelector(selectChainId); @@ -73,10 +73,14 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => { navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { screen: Routes.MODAL.WALLET_ACTIONS, }); - trackEvent(MetaMetricsEvents.ACTIONS_BUTTON_CLICKED, { - text: '', - chain_id: getDecimalChainId(chainId), - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ACTIONS_BUTTON_CLICKED) + .addProperties({ + text: '', + chain_id: getDecimalChainId(chainId), + }) + .build(), + ); break; case Routes.BROWSER_VIEW: navigation.navigate(Routes.BROWSER.HOME, { @@ -124,7 +128,15 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => { /> ); }, - [state, descriptors, navigation, colors, chainId, trackEvent], + [ + state, + descriptors, + navigation, + colors, + chainId, + trackEvent, + createEventBuilder, + ], ); const renderTabBarItems = useCallback( diff --git a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx index 29c48def333..1dc1dc5ca43 100644 --- a/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx +++ b/app/component-library/components/Pickers/PickerNetwork/PickerNetwork.tsx @@ -22,12 +22,18 @@ const PickerNetwork = ({ label, imageSource, hideNetworkName, + isDisabled = false, ...props }: PickerNetworkProps) => { const { styles } = useStyles(stylesheet, { style }); return ( - + ({ createAccountConnectNavDetails: jest.fn(), })); +jest.mock('../../hooks/useOriginSource'); + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), @@ -68,22 +72,24 @@ const mockSelectorState = (state: any) => { const mockTrackEvent = jest.fn(); +(useMetrics as jest.MockedFn).mockReturnValue({ + trackEvent: mockTrackEvent, + createEventBuilder: MetricsEventBuilder.createEventBuilder, + enable: jest.fn(), + addTraitsToUser: jest.fn(), + createDataDeletionTask: jest.fn(), + checkDataDeleteStatus: jest.fn(), + getDeleteRegulationCreationDate: jest.fn(), + getDeleteRegulationId: jest.fn(), + isDataRecorded: jest.fn(), + isEnabled: jest.fn(), + getMetaMetricsId: jest.fn(), +}); + describe('PermissionApproval', () => { beforeEach(() => { - jest.resetAllMocks(); - (useMetrics as jest.MockedFn).mockReturnValue({ - trackEvent: mockTrackEvent, - createEventBuilder: jest.fn(), - enable: jest.fn(), - addTraitsToUser: jest.fn(), - createDataDeletionTask: jest.fn(), - checkDataDeleteStatus: jest.fn(), - getDeleteRegulationCreationDate: jest.fn(), - getDeleteRegulationId: jest.fn(), - isDataRecorded: jest.fn(), - isEnabled: jest.fn(), - getMetaMetricsId: jest.fn(), - }); + jest.clearAllMocks(); + (useOriginSource as jest.Mock).mockImplementation(() => 'IN_APP_BROWSER'); }); it('navigates', async () => { @@ -119,7 +125,12 @@ describe('PermissionApproval', () => { mockApprovalRequest({ type: ApprovalTypes.REQUEST_PERMISSIONS, - requestData: HOST_INFO_MOCK, + requestData: { + ...HOST_INFO_MOCK, + metadata: { + ...HOST_INFO_MOCK.metadata, + }, + }, // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); @@ -143,14 +154,17 @@ describe('PermissionApproval', () => { render(); - expect(mockTrackEvent).toHaveBeenCalledTimes(1); - expect(mockTrackEvent).toHaveBeenCalledWith( + const expectedEvent = MetricsEventBuilder.createEventBuilder( MetaMetricsEvents.CONNECT_REQUEST_STARTED, - { + ) + .addProperties({ number_of_accounts: 3, - source: 'PERMISSION SYSTEM', - }, - ); + source: 'IN_APP_BROWSER', + }) + .build(); + + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + expect(mockTrackEvent).toHaveBeenCalledWith(expectedEvent); }); it('does not navigate if no approval request', async () => { diff --git a/app/components/Approvals/PermissionApproval/PermissionApproval.tsx b/app/components/Approvals/PermissionApproval/PermissionApproval.tsx index a63b45b0076..a0b99f0efdd 100644 --- a/app/components/Approvals/PermissionApproval/PermissionApproval.tsx +++ b/app/components/Approvals/PermissionApproval/PermissionApproval.tsx @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars import { useEffect, useRef } from 'react'; import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest'; import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware'; @@ -7,6 +6,7 @@ import { createAccountConnectNavDetails } from '../../Views/AccountConnect'; import { useSelector } from 'react-redux'; import { selectAccountsLength } from '../../../selectors/accountTrackerController'; import { useMetrics } from '../../../components/hooks/useMetrics'; +import useOriginSource from '../../hooks/useOriginSource'; export interface PermissionApprovalProps { // TODO: Replace "any" with type @@ -15,13 +15,15 @@ export interface PermissionApprovalProps { } const PermissionApproval = (props: PermissionApprovalProps) => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const { approvalRequest } = useApprovalRequest(); const totalAccounts = useSelector(selectAccountsLength); const isProcessing = useRef(false); + const eventSource = useOriginSource({ origin: approvalRequest?.requestData?.metadata?.origin }); + useEffect(() => { - if (approvalRequest?.type !== ApprovalTypes.REQUEST_PERMISSIONS) { + if (approvalRequest?.type !== ApprovalTypes.REQUEST_PERMISSIONS || !eventSource) { isProcessing.current = false; return; } @@ -38,10 +40,14 @@ const PermissionApproval = (props: PermissionApprovalProps) => { isProcessing.current = true; - trackEvent(MetaMetricsEvents.CONNECT_REQUEST_STARTED, { - number_of_accounts: totalAccounts, - source: 'PERMISSION SYSTEM', - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.CONNECT_REQUEST_STARTED) + .addProperties({ + number_of_accounts: totalAccounts, + source: eventSource, + }) + .build(), + ); props.navigation.navigate( ...createAccountConnectNavDetails({ @@ -49,7 +55,14 @@ const PermissionApproval = (props: PermissionApprovalProps) => { permissionRequestId: id, }), ); - }, [approvalRequest, totalAccounts, props.navigation, trackEvent]); + }, [ + approvalRequest, + totalAccounts, + props.navigation, + trackEvent, + createEventBuilder, + eventSource, + ]); return null; }; diff --git a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx index 4630f1984cc..1b1483b2dcc 100644 --- a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx +++ b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx @@ -5,13 +5,26 @@ import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware'; import { ApprovalRequest } from '@metamask/approval-controller'; import SwitchChainApproval from './SwitchChainApproval'; import { networkSwitched } from '../../../actions/onboardNetwork'; +// eslint-disable-next-line import/no-namespace +import * as networks from '../../../util/networks'; +import Engine from '../../../core/Engine'; +const { PreferencesController } = Engine.context; jest.mock('../../Views/confirmations/hooks/useApprovalRequest'); jest.mock('../../../actions/onboardNetwork'); +jest.mock('../../../core/Engine', () => ({ + context: { + PreferencesController: { + setTokenNetworkFilter: jest.fn(), + }, + }, +})); + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useDispatch: () => jest.fn(), + useSelector: jest.fn(), })); const URL_MOCK = 'test.com'; @@ -32,6 +45,7 @@ const mockApprovalRequest = (approvalRequest?: ApprovalRequest) => { describe('SwitchChainApproval', () => { beforeEach(() => { jest.resetAllMocks(); + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(false); }); it('renders', () => { @@ -81,4 +95,29 @@ describe('SwitchChainApproval', () => { networkStatus: true, }); }); + + it('invokes network switched on confirm when portfolio view is enabled', () => { + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true); + const tokenNetworkFilterSpy = jest.spyOn( + PreferencesController, + 'setTokenNetworkFilter', + ); + mockApprovalRequest({ + type: ApprovalTypes.SWITCH_ETHEREUM_CHAIN, + requestData: { + rpcUrl: URL_MOCK, + }, + } as ApprovalRequest<{ + rpcUrl: string; + }>); + + const wrapper = shallow(); + wrapper.find('SwitchCustomNetwork').simulate('confirm'); + expect(tokenNetworkFilterSpy).toHaveBeenCalledTimes(1); + expect(networkSwitched).toHaveBeenCalledTimes(1); + expect(networkSwitched).toHaveBeenCalledWith({ + networkUrl: URL_MOCK, + networkStatus: true, + }); + }); }); diff --git a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx index 9a3310addf9..ff4a9814ce1 100644 --- a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx +++ b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx @@ -4,7 +4,11 @@ import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware'; import ApprovalModal from '../ApprovalModal'; import SwitchCustomNetwork from '../../UI/SwitchCustomNetwork'; import { networkSwitched } from '../../../actions/onboardNetwork'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import Engine from '../../../core/Engine'; +import { selectIsAllNetworks } from '../../../selectors/networkController'; +import { selectTokenNetworkFilter } from '../../../selectors/preferencesController'; +import { isPortfolioViewEnabled } from '../../../util/networks'; const SwitchChainApproval = () => { const { @@ -15,17 +19,34 @@ const SwitchChainApproval = () => { } = useApprovalRequest(); const dispatch = useDispatch(); + const isAllNetworks = useSelector(selectIsAllNetworks); + const tokenNetworkFilter = useSelector(selectTokenNetworkFilter); const onConfirm = useCallback(() => { defaultOnConfirm(); + // If portfolio view is enabled should set network filter + if (isPortfolioViewEnabled()) { + const { PreferencesController } = Engine.context; + PreferencesController.setTokenNetworkFilter({ + ...(isAllNetworks ? tokenNetworkFilter : {}), + [approvalRequest?.requestData?.chainId]: true, + }); + } + dispatch( networkSwitched({ networkUrl: approvalRequest?.requestData?.rpcUrl, networkStatus: true, }), ); - }, [approvalRequest, defaultOnConfirm, dispatch]); + }, [ + approvalRequest, + defaultOnConfirm, + dispatch, + isAllNetworks, + tokenNetworkFilter, + ]); if (approvalRequest?.type !== ApprovalTypes.SWITCH_ETHEREUM_CHAIN) return null; diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 8763bf589d3..32ec533e3ae 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -44,6 +44,7 @@ import { getVersion } from 'react-native-device-info'; import { setCurrentBottomNavRoute, setCurrentRoute, + onNavigationReady, } from '../../../actions/navigation'; import { findRouteNameFromNavigatorState } from '../../../util/general'; import { Authentication } from '../../../core/'; @@ -608,6 +609,13 @@ const App = (props) => { } }; + useEffect(() => { + if (prevNavigator.current || !navigator) return; + + endTrace({ name: TraceName.NavInit }); + endTrace({ name: TraceName.UIStartup }); + }, [navigator]); + useEffect(() => { if (prevNavigator.current || !navigator) return; const appTriggeredAuth = async () => { @@ -650,15 +658,9 @@ const App = (props) => { ); } }; - appTriggeredAuth() - .catch((error) => { - Logger.error(error, 'App: Error in appTriggeredAuth'); - }) - .finally(() => { - endTrace({ name: TraceName.NavInit }); - - endTrace({ name: TraceName.UIStartup }); - }); + appTriggeredAuth().catch((error) => { + Logger.error(error, 'App: Error in appTriggeredAuth'); + }); }, [navigator, queueOfHandleDeeplinkFunctions]); const handleDeeplink = useCallback(({ error, params, uri }) => { @@ -879,6 +881,11 @@ const App = (props) => { } }; + /** + * Triggers when the navigation is ready + */ + const onNavigationReadyHandler = () => dispatch(onNavigationReady()); + return supressRender ? null : ( <> { @@ -904,6 +911,7 @@ const App = (props) => { const currentRoute = findRouteNameFromNavigatorState(state.routes); triggerSetCurrentRoute(currentRoute); }} + onReady={onNavigationReadyHandler} > ( ); const HomeTabs = () => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const drawerRef = useRef(null); const [isKeyboardHidden, setIsKeyboardHidden] = useState(true); @@ -439,10 +439,14 @@ const HomeTabs = () => { home: { tabBarIconKey: TabBarIconKey.Wallet, callback: () => { - trackEvent(MetaMetricsEvents.WALLET_OPENED, { - number_of_accounts: accountsLength, - chain_id: getDecimalChainId(chainId), - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_OPENED) + .addProperties({ + number_of_accounts: accountsLength, + chain_id: getDecimalChainId(chainId), + }) + .build(), + ); }, rootScreenName: Routes.WALLET_VIEW, }, @@ -453,27 +457,39 @@ const HomeTabs = () => { browser: { tabBarIconKey: TabBarIconKey.Browser, callback: () => { - trackEvent(MetaMetricsEvents.BROWSER_OPENED, { - number_of_accounts: accountsLength, - chain_id: getDecimalChainId(chainId), - source: 'Navigation Tab', - active_connected_dapp: activeConnectedDapp, - number_of_open_tabs: amountOfBrowserOpenTabs, - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.BROWSER_OPENED) + .addProperties({ + number_of_accounts: accountsLength, + chain_id: getDecimalChainId(chainId), + source: 'Navigation Tab', + active_connected_dapp: activeConnectedDapp, + number_of_open_tabs: amountOfBrowserOpenTabs, + }) + .build(), + ); }, rootScreenName: Routes.BROWSER_VIEW, }, activity: { tabBarIconKey: TabBarIconKey.Activity, callback: () => { - trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_TRANSACTION_HISTORY); + trackEvent( + createEventBuilder( + MetaMetricsEvents.NAVIGATION_TAPS_TRANSACTION_HISTORY, + ).build(), + ); }, rootScreenName: Routes.TRANSACTIONS_VIEW, }, settings: { tabBarIconKey: TabBarIconKey.Setting, callback: () => { - trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_SETTINGS); + trackEvent( + createEventBuilder( + MetaMetricsEvents.NAVIGATION_TAPS_SETTINGS, + ).build(), + ); }, rootScreenName: Routes.SETTINGS_VIEW, unmountOnBlur: true, diff --git a/app/components/Nav/Main/RootRPCMethodsUI.js b/app/components/Nav/Main/RootRPCMethodsUI.js index 712919e682c..d774fd86508 100644 --- a/app/components/Nav/Main/RootRPCMethodsUI.js +++ b/app/components/Nav/Main/RootRPCMethodsUI.js @@ -59,7 +59,7 @@ import TemplateConfirmationModal from '../../Approvals/TemplateConfirmationModal import { selectTokenList } from '../../../selectors/tokenListController'; import { selectTokens } from '../../../selectors/tokensController'; import { getDeviceId } from '../../../core/Ledger/Ledger'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; import { createLedgerTransactionModalNavDetails } from '../../UI/LedgerModals/LedgerTransactionModal'; import ExtendedKeyringTypes from '../../../constants/keyringTypes'; import Confirm from '../../../components/Views/confirmations/Confirm'; @@ -73,6 +73,7 @@ import { updateSwapsTransaction } from '../../../util/swaps/swaps-transactions'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import InstallSnapApproval from '../../Approvals/InstallSnapApproval'; +import { getGlobalEthQuery } from '../../../util/networks/global-network'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import SnapAccountCustomNameApproval from '../../Approvals/SnapAccountCustomNameApproval'; @@ -131,7 +132,7 @@ export const useSwapConfirmedEvent = ({ trackSwaps }) => { }; const RootRPCMethodsUI = (props) => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const [transactionModalType, setTransactionModalType] = useState(undefined); const tokenList = useSelector(selectTokenList); const setTransactionObject = props.setTransactionObject; @@ -160,7 +161,7 @@ const RootRPCMethodsUI = (props) => { ({ id }) => id === approvalTransactionMetaId, ); - const ethQuery = Engine.getGlobalEthQuery(); + const ethQuery = getGlobalEthQuery(); const ethBalance = await query(ethQuery, 'getBalance', [ props.selectedAddress, @@ -233,26 +234,53 @@ const RootRPCMethodsUI = (props) => { ); const parameters = { - ...analyticsParams, time_to_mine: timeToMine, estimated_vs_used_gasRatio: estimatedVsUsedGasRatio, quote_vs_executionRatio: quoteVsExecutionRatio, token_to_amount_received: tokenToAmountReceived.toString(), is_smart_transaction: props.shouldUseSmartTransaction, ...smartTransactionMetricsProperties, + available_quotes: analyticsParams.available_quotes, + best_quote_source: analyticsParams.best_quote_source, + chain_id: analyticsParams.chain_id, + custom_slippage: analyticsParams.custom_slippage, + network_fees_USD: analyticsParams.network_fees_USD, + other_quote_selected: analyticsParams.other_quote_selected, + request_type: analyticsParams.request_type, + token_from: analyticsParams.token_from, + token_to: analyticsParams.token_to, + }; + const sensitiveParameters = { + token_from_amount: analyticsParams.token_from_amount, + token_to_amount: analyticsParams.token_to_amount, + network_fees_ETH: analyticsParams.network_fees_ETH, }; Logger.log('Swaps', 'Sending metrics event', event); - trackEvent(event, { sensitiveProperties: { ...parameters } }); + trackEvent( + createEventBuilder(event) + .addProperties({ ...parameters }) + .addSensitiveProperties({ ...sensitiveParameters }) + .build(), + ); } catch (e) { Logger.error(e, MetaMetricsEvents.SWAP_TRACKING_FAILED); - trackEvent(MetaMetricsEvents.SWAP_TRACKING_FAILED, { - error: e, - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.SWAP_TRACKING_FAILED) + .addProperties({ + error: e, + }) + .build(), + ); } }, - [props.selectedAddress, props.shouldUseSmartTransaction, trackEvent], + [ + props.selectedAddress, + props.shouldUseSmartTransaction, + trackEvent, + createEventBuilder, + ], ); const { addTransactionMetaIdForListening } = useSwapConfirmedEvent({ @@ -307,7 +335,7 @@ const RootRPCMethodsUI = (props) => { transactionId: transactionMeta.id, deviceId, // eslint-disable-next-line no-empty-function - onConfirmationComplete: () => {}, + onConfirmationComplete: () => { }, type: 'signTransaction', }), ); @@ -326,7 +354,11 @@ const RootRPCMethodsUI = (props) => { ); Logger.error(error, 'error while trying to send transaction (Main)'); } else { - trackEvent(MetaMetricsEvents.QR_HARDWARE_TRANSACTION_CANCELED); + trackEvent( + createEventBuilder( + MetaMetricsEvents.QR_HARDWARE_TRANSACTION_CANCELED, + ).build(), + ); } } }, @@ -336,6 +368,7 @@ const RootRPCMethodsUI = (props) => { trackEvent, swapsTransactions, addTransactionMetaIdForListening, + createEventBuilder, ], ); @@ -359,6 +392,7 @@ const RootRPCMethodsUI = (props) => { autoSign(transactionMeta); } else { const { + networkClientId, txParams: { value, gas, gasPrice, data }, } = transactionMeta; const { AssetsContractController } = Engine.context; @@ -370,7 +404,7 @@ const RootRPCMethodsUI = (props) => { data && data !== '0x' && to && - (await getMethodData(data)).name === TOKEN_METHOD_TRANSFER + (await getMethodData(data, networkClientId)).name === TOKEN_METHOD_TRANSFER ) { let asset = props.tokens.find(({ address }) => toLowerCaseEquals(address, to), @@ -413,6 +447,7 @@ const RootRPCMethodsUI = (props) => { id: transactionMeta.id, origin: transactionMeta.origin, securityAlertResponse: transactionMeta.securityAlertResponse, + networkClientId, ...transactionMeta.txParams, }); } else { @@ -425,6 +460,7 @@ const RootRPCMethodsUI = (props) => { id: transactionMeta.id, origin: transactionMeta.origin, securityAlertResponse: transactionMeta.securityAlertResponse, + networkClientId, ...transactionMeta.txParams, }); } @@ -541,7 +577,7 @@ RootRPCMethodsUI.propTypes = { }; const mapStateToProps = (state) => ({ - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), chainId: selectChainId(state), tokens: selectTokens(state), providerType: selectProviderType(state), diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 8d463aae71b..b8ba760da0c 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -58,6 +58,7 @@ import { useMinimumVersions } from '../../hooks/MinimumVersions'; import navigateTermsOfUse from '../../../util/termsOfUse/termsOfUse'; import { selectChainId, + selectNetworkClientId, selectNetworkConfigurations, selectProviderConfig, selectProviderType, @@ -82,6 +83,8 @@ import { } from '../../../util/transaction-controller'; import isNetworkUiRedesignEnabled from '../../../util/networks/isNetworkUiRedesignEnabled'; import { useConnectionHandler } from '../../../util/navigation/useConnectionHandler'; +import { AssetPollingProvider } from '../../hooks/AssetPolling/AssetPollingProvider'; +import { getGlobalEthQuery } from '../../../util/networks/global-network'; const Stack = createStackNavigator(); @@ -116,6 +119,8 @@ const Main = (props) => { useEnableAutomaticSecurityChecks(); useMinimumVersions(); + const { chainId, networkClientId, showIncomingTransactionsNetworks } = props; + useEffect(() => { if (DEPRECATED_NETWORKS.includes(props.chainId)) { setShowDeprecatedAlert(true); @@ -125,19 +130,17 @@ const Main = (props) => { }, [props.chainId]); useEffect(() => { - const chainId = props.chainId; + stopIncomingTransactionPolling(); - if (props.showIncomingTransactionsNetworks[chainId]) { - startIncomingTransactionPolling(); - } else { - stopIncomingTransactionPolling(); + if (showIncomingTransactionsNetworks[chainId]) { + startIncomingTransactionPolling([chainId]); } - }, [props.showIncomingTransactionsNetworks, props.chainId]); + }, [chainId, networkClientId, showIncomingTransactionsNetworks]); const checkInfuraAvailability = useCallback(async () => { if (props.providerType !== 'rpc') { try { - const ethQuery = Engine.getGlobalEthQuery(); + const ethQuery = getGlobalEthQuery(); await query(ethQuery, 'blockNumber', []); props.setInfuraAvailabilityNotBlocked(); } catch (e) { @@ -175,11 +178,11 @@ const Main = (props) => { removeNotVisibleNotifications(); BackgroundTimer.runBackgroundTimer(async () => { - await updateIncomingTransactions(); + await updateIncomingTransactions([props.chainId]); }, AppConstants.TX_CHECK_BACKGROUND_FREQUENCY); } }, - [backgroundMode, removeNotVisibleNotifications], + [backgroundMode, removeNotVisibleNotifications, props.chainId], ); const initForceReload = () => { @@ -362,35 +365,37 @@ const Main = (props) => { return ( - - {!forceReload ? ( - - ) : ( - renderLoader() - )} - - - - - - - {renderDeprecatedNetworkAlert( - props.chainId, - props.backUpSeedphraseVisible, - )} - - - - + + + {!forceReload ? ( + + ) : ( + renderLoader() + )} + + + + + + + {renderDeprecatedNetworkAlert( + props.chainId, + props.backUpSeedphraseVisible, + )} + + + + + ); }; @@ -447,6 +452,10 @@ Main.propTypes = { * backup seed phrase modal visible */ backUpSeedphraseVisible: PropTypes.bool, + /** + * ID of the global network client + */ + networkClientId: PropTypes.string, }; const mapStateToProps = (state) => ({ @@ -454,6 +463,7 @@ const mapStateToProps = (state) => ({ selectShowIncomingTransactionNetworks(state), providerType: selectProviderType(state), chainId: selectChainId(state), + networkClientId: selectNetworkClientId(state), backUpSeedphraseVisible: state.user.backUpSeedphraseVisible, }); diff --git a/app/components/Nav/Main/index.test.tsx b/app/components/Nav/Main/index.test.tsx index 33d004c74fe..1d6ed7941c9 100644 --- a/app/components/Nav/Main/index.test.tsx +++ b/app/components/Nav/Main/index.test.tsx @@ -10,7 +10,7 @@ import { MetaMetricsEvents } from '../../hooks/useMetrics'; import { renderHookWithProvider } from '../../../util/test/renderWithProvider'; import Engine from '../../../core/Engine'; -jest.mock('../../../core/Engine.ts', () => ({ +jest.mock('../../../core/Engine', () => ({ controllerMessenger: { subscribeOnceIf: jest.fn(), }, diff --git a/app/components/UI/AccountApproval/index.js b/app/components/UI/AccountApproval/index.js index a486c783226..e9e26c8682b 100644 --- a/app/components/UI/AccountApproval/index.js +++ b/app/components/UI/AccountApproval/index.js @@ -1,10 +1,6 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; -import { - InteractionManager, - TouchableOpacity, - View, -} from 'react-native'; +import { InteractionManager, TouchableOpacity, View } from 'react-native'; import { connect } from 'react-redux'; import { strings } from '../../../../locales/i18n'; import Text from '../../../component-library/components/Texts/Text'; @@ -26,7 +22,7 @@ import Routes from '../../../constants/navigation/Routes'; import Engine from '../../../core/Engine'; import SDKConnect from '../../../core/SDKConnect/SDKConnect'; import { selectAccountsLength } from '../../../selectors/accountTrackerController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; import { selectChainId, selectProviderType, @@ -38,7 +34,8 @@ import { getDecimalChainId } from '../../../util/networks'; import { ThemeContext, mockTheme } from '../../../util/theme'; import ShowWarningBanner from './showWarningBanner'; import createStyles from './styles'; -import { SourceType } from '../../../components/hooks/useMetrics/useMetrics.types'; +import { SourceType } from '../../hooks/useMetrics/useMetrics.types'; +import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder'; /** * Account access approval component @@ -165,8 +162,11 @@ class AccountApproval extends PureComponent { this.checkUrlFlaggedAsPhishing(hostname); this.props.metrics.trackEvent( - MetaMetricsEvents.CONNECT_REQUEST_STARTED, - this.getAnalyticsParams(), + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.CONNECT_REQUEST_STARTED, + ) + .addProperties(this.getAnalyticsParams()) + .build(), ); }; @@ -202,8 +202,11 @@ class AccountApproval extends PureComponent { this.props.onCancel(); this.props.metrics.trackEvent( - MetaMetricsEvents.CONNECT_REQUEST_OTPFAILURE, - this.getAnalyticsParams(), + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.CONNECT_REQUEST_OTPFAILURE, + ) + .addProperties(this.getAnalyticsParams()) + .build(), ); // Navigate to feedback modal @@ -223,8 +226,11 @@ class AccountApproval extends PureComponent { this.props.onConfirm(); this.props.metrics.trackEvent( - MetaMetricsEvents.CONNECT_REQUEST_COMPLETED, - this.getAnalyticsParams(), + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.CONNECT_REQUEST_COMPLETED, + ) + .addProperties(this.getAnalyticsParams()) + .build(), ); this.showWalletConnectNotification(true); }; @@ -234,10 +240,12 @@ class AccountApproval extends PureComponent { */ onCancel = () => { this.props.metrics.trackEvent( - MetaMetricsEvents.CONNECT_REQUEST_CANCELLED, - this.getAnalyticsParams(), + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.CONNECT_REQUEST_CANCELLED, + ) + .addProperties(this.getAnalyticsParams()) + .build(), ); - if (this.props.currentPageInformation.channelId) { SDKConnect.getInstance().removeChannel( this.props.currentPageInformation.channelId, @@ -403,7 +411,7 @@ class AccountApproval extends PureComponent { const mapStateToProps = (state) => ({ accountsLength: selectAccountsLength(state), tokensLength: selectTokensLength(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), networkType: selectProviderType(state), chainId: selectChainId(state), }); diff --git a/app/components/UI/AccountApproval/index.test.tsx b/app/components/UI/AccountApproval/index.test.tsx index 0afe79dd39e..29e438ead16 100644 --- a/app/components/UI/AccountApproval/index.test.tsx +++ b/app/components/UI/AccountApproval/index.test.tsx @@ -48,6 +48,13 @@ const mockInitialState = { }, }, }, + TokensController: { + allTokens: { + '0x1': { + '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756': [], + }, + }, + }, }, }, }; diff --git a/app/components/UI/AccountApproval/showWarningBanner.tsx b/app/components/UI/AccountApproval/showWarningBanner.tsx index 439fc7326d6..ce3a7b54420 100644 --- a/app/components/UI/AccountApproval/showWarningBanner.tsx +++ b/app/components/UI/AccountApproval/showWarningBanner.tsx @@ -18,7 +18,8 @@ import { } from '../../../component-library/components/Icons/Icon'; import { CONNECTING_TO_A_DECEPTIVE_SITE } from '../../../constants/urls'; import { AccordionHeaderHorizontalAlignment } from '../../../component-library/components/Accordions/Accordion'; -import { MetaMetrics } from '../../../core/Analytics'; +import { MetaMetrics, MetaMetricsEvents } from '../../../core/Analytics'; +import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder'; const descriptionArray = [ strings('accounts.fake_metamask'), @@ -29,12 +30,15 @@ const descriptionArray = [ const goToLearnMore = () => { Linking.openURL(CONNECTING_TO_A_DECEPTIVE_SITE); MetaMetrics.getInstance().trackEvent( - { category: 'EXTERNAL_LINK_CLICKED' }, - { - location: 'dapp_connection_request', - text: 'Learn More', - url_domain: CONNECTING_TO_A_DECEPTIVE_SITE, - }, + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.EXTERNAL_LINK_CLICKED, + ) + .addProperties({ + location: 'dapp_connection_request', + text: 'Learn More', + url_domain: CONNECTING_TO_A_DECEPTIVE_SITE, + }) + .build(), ); }; diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx index 82014a8debc..c650e8640f5 100644 --- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx +++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.test.tsx @@ -38,8 +38,12 @@ const mockInitialState: DeepPartial = { }, }, TokenBalancesController: { - contractBalances: { - '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x5', + tokenBalances: { + '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': { + '0x5': { + '0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x2b46', + }, + }, }, }, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, diff --git a/app/components/UI/AccountInfoCard/index.test.tsx b/app/components/UI/AccountInfoCard/index.test.tsx index 4806be74eaf..d7567ef9d06 100644 --- a/app/components/UI/AccountInfoCard/index.test.tsx +++ b/app/components/UI/AccountInfoCard/index.test.tsx @@ -67,7 +67,7 @@ const mockInitialState: DeepPartial = { }), }, TokenBalancesController: { - contractBalances: {}, + tokenBalances: {}, }, }, }, diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index 1a73bb38746..fa806567139 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -36,7 +36,7 @@ import { selectChainId } from '../../../selectors/networkController'; import { selectCurrentCurrency } from '../../../selectors/currencyRateController'; import { selectInternalAccounts, - selectSelectedInternalAccountChecksummedAddress, + selectSelectedInternalAccountFormattedAddress, } from '../../../selectors/accountsController'; import { createAccountSelectorNavDetails } from '../../Views/AccountSelector'; import Text, { @@ -289,7 +289,11 @@ class AccountOverview extends PureComponent { }); setTimeout(() => this.props.protectWalletModalVisible(), 2000); - this.props.metrics.trackEvent(MetaMetricsEvents.WALLET_COPIED_ADDRESS); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.WALLET_COPIED_ADDRESS) + .build(), + ); }; doENSLookup = async () => { @@ -322,9 +326,12 @@ class AccountOverview extends PureComponent { screen: Routes.BROWSER.VIEW, params, }); - this.props.metrics.trackEvent(MetaMetricsEvents.PORTFOLIO_LINK_CLICKED, { - portfolioUrl: AppConstants.PORTFOLIO.URL, - }); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.PORTFOLIO_LINK_CLICKED) + .addProperties({ portfolioUrl: AppConstants.PORTFOLIO.URL }) + .build(), + ); }; render() { @@ -443,7 +450,7 @@ class AccountOverview extends PureComponent { } const mapStateToProps = (state) => ({ - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), internalAccounts: selectInternalAccounts(state), currentCurrency: selectCurrentCurrency(state), chainId: selectChainId(state), diff --git a/app/components/UI/AccountOverview/index.test.tsx b/app/components/UI/AccountOverview/index.test.tsx index 49fb5077401..9df49d8d11b 100644 --- a/app/components/UI/AccountOverview/index.test.tsx +++ b/app/components/UI/AccountOverview/index.test.tsx @@ -11,7 +11,7 @@ import { const mockedEngine = Engine; -jest.mock('../../../core/Engine.ts', () => { +jest.mock('../../../core/Engine', () => { const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = jest.requireActual('../../../util/test/accountsControllerTestUtils'); return { diff --git a/app/components/UI/AccountRightButton/index.tsx b/app/components/UI/AccountRightButton/index.tsx index 4b21c8a6702..b144242f5f2 100644 --- a/app/components/UI/AccountRightButton/index.tsx +++ b/app/components/UI/AccountRightButton/index.tsx @@ -26,7 +26,7 @@ import BadgeWrapper from '../../../component-library/components/Badges/BadgeWrap import { selectProviderConfig } from '../../../selectors/networkController'; import Routes from '../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { AccountOverviewSelectorsIDs } from '../../../../e2e/selectors/AccountOverview.selectors'; +import { AccountOverviewSelectorsIDs } from '../../../../e2e/selectors/Browser/AccountOverview.selectors'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { useNetworkInfo } from '../../../selectors/selectedNetworkController'; import UrlParser from 'url-parse'; @@ -59,7 +59,7 @@ const AccountRightButton = ({ // Placeholder ref for dismissing keyboard. Works when the focused input is within a Webview. const placeholderInputRef = useRef(null); const { navigate } = useNavigation(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const [isKeyboardVisible, setIsKeyboardVisible] = useState(false); // TODO: Replace "any" with type @@ -118,9 +118,13 @@ const AccountRightButton = ({ navigate(Routes.MODAL.ROOT_MODAL_FLOW, { screen: Routes.SHEET.NETWORK_SELECTOR, }); - trackEvent(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED, { - chain_id: getDecimalChainId(providerConfig.chainId), - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED) + .addProperties({ + chain_id: getDecimalChainId(providerConfig.chainId), + }) + .build(), + ); } else { onPress?.(); } @@ -132,6 +136,7 @@ const AccountRightButton = ({ navigate, providerConfig.chainId, trackEvent, + createEventBuilder, ]); const route = useRoute, string>>(); diff --git a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx index 506d7e6e37d..ad88315241f 100644 --- a/app/components/UI/AccountSelectorList/AccountSelector.test.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelector.test.tsx @@ -5,7 +5,7 @@ import renderWithProvider from '../../../util/test/renderWithProvider'; import AccountSelectorList from './AccountSelectorList'; import { useAccounts } from '../../../components/hooks/useAccounts'; import { View } from 'react-native'; -import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; +import { AccountListBottomSheetSelectorsIDs } from '../../../../e2e/selectors/wallet/AccountListBottomSheet.selectors'; import { backgroundState } from '../../../util/test/initial-root-state'; import { regex } from '../../../../app/util/regex'; import { @@ -18,6 +18,9 @@ import { mockNetworkState } from '../../../util/test/network'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { AccountSelectorListProps } from './AccountSelectorList.types'; +// eslint-disable-next-line import/no-namespace +import * as Utils from '../../hooks/useAccounts/utils'; + const BUSINESS_ACCOUNT = '0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'; const PERSONAL_ACCOUNT = '0xd018538C87232FF95acbCe4870629b75640a78E7'; @@ -125,6 +128,16 @@ const renderComponent = ( describe('AccountSelectorList', () => { beforeEach(() => { + jest.spyOn(Utils, 'getAccountBalances').mockReturnValueOnce({ + balanceETH: '1', + balanceFiat: '$3200.00', + balanceWeiHex: '', + }); + jest.spyOn(Utils, 'getAccountBalances').mockReturnValueOnce({ + balanceETH: '2', + balanceFiat: '$6400.00', + balanceWeiHex: '', + }); onSelectAccount.mockClear(); onRemoveImportedAccount.mockClear(); }); @@ -140,10 +153,10 @@ describe('AccountSelectorList', () => { await waitFor(async () => { const businessAccountItem = await queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); const personalAccountItem = await queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -183,7 +196,7 @@ describe('AccountSelectorList', () => { expect(accounts.length).toBe(1); const businessAccountItem = await queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -251,7 +264,7 @@ describe('AccountSelectorList', () => { await waitFor(() => { const businessAccountItem = queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined(); @@ -272,7 +285,7 @@ describe('AccountSelectorList', () => { await waitFor(() => { const businessAccountItem = queryByTestId( - `${AccountListViewSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, + `${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`, ); expect(within(businessAccountItem).queryByText(regex.eth(1))).toBeNull(); diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index 30b8241836f..499a3eacb96 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -5,7 +5,6 @@ import { FlatList } from 'react-native-gesture-handler'; import { useSelector } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { Hex } from '@metamask/utils'; // External dependencies. import { selectInternalAccounts } from '../../../selectors/accountsController'; @@ -36,7 +35,7 @@ import Routes from '../../../constants/navigation/Routes'; // Internal dependencies. import { AccountSelectorListProps } from './AccountSelectorList.types'; import styleSheet from './AccountSelectorList.styles'; -import { AccountListViewSelectorsIDs } from '../../../../e2e/selectors/AccountListView.selectors'; +import { AccountListBottomSheetSelectorsIDs } from '../../../../e2e/selectors/wallet/AccountListBottomSheet.selectors'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; const AccountSelectorList = ({ @@ -82,7 +81,7 @@ const AccountSelectorList = ({ return ( + { const { styles } = useStyles(styleSheet, {}); const dispatch = useDispatch(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const handleShowAlert = (config: { isVisible: boolean; @@ -59,7 +59,9 @@ const AddressCopy = () => { }); setTimeout(() => handleProtectWalletModalVisible(), 2000); - trackEvent(MetaMetricsEvents.WALLET_COPIED_ADDRESS); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_COPIED_ADDRESS).build(), + ); }; return ( diff --git a/app/components/UI/AddressInputs/index.js b/app/components/UI/AddressInputs/index.js index f1f59b4af4f..5c7425e8bbf 100644 --- a/app/components/UI/AddressInputs/index.js +++ b/app/components/UI/AddressInputs/index.js @@ -15,7 +15,7 @@ import { strings } from '../../../../locales/i18n'; import { hasZeroWidthPoints } from '../../../util/confusables'; import { useTheme } from '../../../util/theme'; import AddToAddressBookWrapper from '../AddToAddressBookWrapper/AddToAddressBookWrapper'; -import { SendViewSelectorsIDs } from '../../../../e2e/selectors/SendView.selectors'; +import { SendViewSelectorsIDs } from '../../../../e2e/selectors/SendFlow/SendView.selectors'; import Text, { TextVariant, } from '../../../component-library/components/Texts/Text'; diff --git a/app/components/UI/AddressInputs/index.test.jsx b/app/components/UI/AddressInputs/index.test.jsx index 73a80cda421..d3c6be0d158 100644 --- a/app/components/UI/AddressInputs/index.test.jsx +++ b/app/components/UI/AddressInputs/index.test.jsx @@ -4,7 +4,7 @@ import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { AddressFrom, AddressTo } from './index'; import { backgroundState } from '../../../util/test/initial-root-state'; -import { AddAddressModalSelectorsIDs } from '../../../../e2e/selectors/Modals/AddAddressModal.selectors'; +import { AddAddressModalSelectorsIDs } from '../../../../e2e/selectors/SendFlow/AddAddressModal.selectors'; const initialState = { settings: {}, diff --git a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx index 415cff34f01..0b260915412 100644 --- a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx +++ b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.test.tsx @@ -1,10 +1,12 @@ import React from 'react'; import renderWithProvider from '../../../util/test/renderWithProvider'; -import ApprovalTagUrl from './ApprovalTagUrl'; +import ApprovalTagUrl, { APPROVAL_TAG_URL_ORIGIN_PILL } from './ApprovalTagUrl'; import { backgroundState } from '../../../util/test/initial-root-state'; +import { INTERNAL_ORIGINS } from '../../../constants/transaction'; const ADDRESS_MOCK = '0x1234567890abcdef1234567890abcdef12345678'; const DOMAIN_MOCK = 'metamask.github.io'; + const mockInitialState = { settings: {}, engine: { @@ -19,7 +21,7 @@ const mockInitialState = { describe('ApprovalTagUrl', () => { it('renders correctly', () => { - const { toJSON } = renderWithProvider( + const { toJSON, getByTestId } = renderWithProvider( { ); expect(toJSON()).toMatchSnapshot(); + expect(getByTestId(APPROVAL_TAG_URL_ORIGIN_PILL)).toBeDefined(); + }); + + it('does not render when origin is an internal origin', () => { + const { queryByTestId } = renderWithProvider( + , + { state: mockInitialState }, + ); + + expect(queryByTestId(APPROVAL_TAG_URL_ORIGIN_PILL)).toBeNull(); }); }); diff --git a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx index 126f3132b17..dc7ebc8aa2d 100644 --- a/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx +++ b/app/components/UI/ApprovalTagUrl/ApprovalTagUrl.tsx @@ -11,6 +11,7 @@ import { selectAccountsByChainId } from '../../../selectors/accountTrackerContro import { getHost, prefixUrlWithProtocol } from '../../../util/browser'; import useFavicon from '../../hooks/useFavicon/useFavicon'; import stylesheet from './ApprovalTagUrl.styles'; +import { INTERNAL_ORIGINS } from '../../../constants/transaction'; const { ORIGIN_DEEPLINK, ORIGIN_QR_CODE } = AppConstants.DEEPLINKS; export const APPROVAL_TAG_URL_ORIGIN_PILL = 'APPROVAL_TAG_URL_ORIGIN_PILL'; @@ -51,14 +52,18 @@ const ApprovalTagUrl = ({ const domainTitle = useMemo(() => { let title = ''; - if (currentEnsName || origin || url) { - title = prefixUrlWithProtocol(currentEnsName || origin || getHost(url)); + if (currentEnsName) { + title = prefixUrlWithProtocol(currentEnsName); + } else if (origin && isOriginDeepLink) { + title = prefixUrlWithProtocol(origin); + } else if (url) { + title = prefixUrlWithProtocol(getHost(url)); } else { title = ''; } return title; - }, [currentEnsName, origin, url]); + }, [currentEnsName, origin, url, isOriginDeepLink]); const faviconSource = useFavicon(origin as string) as | { uri: string } @@ -72,7 +77,9 @@ const ApprovalTagUrl = ({ uri: '', }; - if (origin && !isOriginDeepLink) { + const showOrigin = origin && !isOriginDeepLink && !INTERNAL_ORIGINS.includes(origin); + + if (showOrigin) { return ( { const actualNav = jest.requireActual('@react-navigation/native'); return { ...actualNav, useNavigation: () => ({ - navigate: jest.fn(), + navigate: mockNavigate, }), }; }); @@ -67,13 +95,21 @@ jest.mock('../../hooks/useStyles', () => ({ }), })); -// Mock the navigation object. -const navigation = { - navigate: jest.fn(), -}; +jest.mock('../../../core/Engine', () => ({ + context: { + NetworkController: { + getNetworkConfigurationByChainId: jest + .fn() + .mockReturnValue(mockNetworkConfiguration), + setActiveNetwork: jest.fn().mockResolvedValue(undefined), + }, + }, +})); + const asset = { balance: '400', balanceFiat: '1500', + chainId: MOCK_CHAIN_ID, logo: 'https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg', symbol: 'ETH', name: 'Ethereum', @@ -86,64 +122,100 @@ const asset = { }; describe('AssetOverview', () => { + beforeEach(() => { + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(false); + }); + it('should render correctly', async () => { const container = renderWithProvider( - , + , { state: mockInitialState }, ); expect(container).toMatchSnapshot(); }); + it('should render correctly when portfolio view is enabled', async () => { + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true); + + const container = renderWithProvider( + , + { state: mockInitialState }, + ); + expect(container).toMatchSnapshot(); + }); + + it('should handle buy button press', async () => { + const { getByTestId } = renderWithProvider( + , + { state: mockInitialState }, + ); + + const buyButton = getByTestId(TokenOverviewSelectorsIDs.BUY_BUTTON); + fireEvent.press(buyButton); + + expect(navigate).toHaveBeenCalledWith( + ...createBuyNavigationDetails({ + address: asset.address, + chainId: getDecimalChainId(MOCK_CHAIN_ID), + }), + ); + }); + it('should handle send button press', async () => { const { getByTestId } = renderWithProvider( - , + , { state: mockInitialState }, ); const sendButton = getByTestId('token-send-button'); fireEvent.press(sendButton); - expect(navigation.navigate).toHaveBeenCalledWith('SendFlowView', {}); + expect(navigate).toHaveBeenCalledWith('SendFlowView', {}); }); it('should handle swap button press', async () => { const { getByTestId } = renderWithProvider( - , + , { state: mockInitialState }, ); const swapButton = getByTestId('token-swap-button'); fireEvent.press(swapButton); - expect(navigation.navigate).toHaveBeenCalledWith('Swaps', { - params: { - sourcePage: 'MainView', - sourceToken: asset.address, - }, - screen: 'SwapsAmountView', - }); + if (isPortfolioViewEnabled()) { + expect(navigate).toHaveBeenCalledTimes(3); + expect(navigate).toHaveBeenNthCalledWith(1, 'RampBuy', { + screen: 'GetStarted', + params: { + address: asset.address, + chainId: getDecimalChainId(MOCK_CHAIN_ID), + }, + }); + expect(navigate).toHaveBeenNthCalledWith(2, 'SendFlowView', {}); + expect(navigate).toHaveBeenNthCalledWith(3, 'Swaps', { + screen: 'SwapsAmountView', + params: { + sourcePage: 'MainView', + address: asset.address, + chainId: MOCK_CHAIN_ID, + }, + }); + } else { + expect(navigate).toHaveBeenCalledWith('Swaps', { + screen: 'SwapsAmountView', + params: { + sourcePage: 'MainView', + sourceToken: asset.address, + chainId: '0x1', + }, + }); + } }); it('should not render swap button if displaySwapsButton is false', async () => { const { queryByTestId } = renderWithProvider( , @@ -153,4 +225,18 @@ describe('AssetOverview', () => { const swapButton = queryByTestId('token-swap-button'); expect(swapButton).toBeNull(); }); + + it('should not render buy button if displayBuyButton is false', async () => { + const { queryByTestId } = renderWithProvider( + , + { state: mockInitialState }, + ); + + const buyButton = queryByTestId(TokenOverviewSelectorsIDs.BUY_BUTTON); + expect(buyButton).toBeNull(); + }); }); diff --git a/app/components/UI/AssetOverview/AssetOverview.tsx b/app/components/UI/AssetOverview/AssetOverview.tsx index 48b455adc8e..cbcd738dca6 100644 --- a/app/components/UI/AssetOverview/AssetOverview.tsx +++ b/app/components/UI/AssetOverview/AssetOverview.tsx @@ -1,24 +1,37 @@ -import { zeroAddress } from 'ethereumjs-util'; import React, { useCallback, useEffect } from 'react'; import { TouchableOpacity, View } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; import { useDispatch, useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; +import { getNativeTokenAddress } from '@metamask/assets-controllers'; import { strings } from '../../../../locales/i18n'; -import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/TokenOverview.selectors'; +import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/wallet/TokenOverview.selectors'; import { newAssetTransaction } from '../../../actions/transaction'; import AppConstants from '../../../core/AppConstants'; import Engine from '../../../core/Engine'; import { selectChainId, selectTicker, + selectNativeCurrencyByChainId, } from '../../../selectors/networkController'; import { selectConversionRate, selectCurrentCurrency, + selectCurrencyRates, } from '../../../selectors/currencyRateController'; -import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; +import { + selectContractExchangeRates, + selectTokenMarketData, +} from '../../../selectors/tokenRatesController'; import { selectAccountsByChainId } from '../../../selectors/accountTrackerController'; -import { selectContractBalances } from '../../../selectors/tokenBalancesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { + selectContractBalances, + selectTokensBalances, +} from '../../../selectors/tokenBalancesController'; +import { + selectSelectedInternalAccountAddress, + selectSelectedInternalAccountFormattedAddress, +} from '../../../selectors/accountsController'; import Logger from '../../../util/Logger'; import { safeToChecksumAddress } from '../../../util/address'; import { @@ -45,48 +58,75 @@ import Routes from '../../../constants/navigation/Routes'; import TokenDetails from './TokenDetails'; import { RootState } from '../../../reducers'; import useGoToBridge from '../Bridge/utils/useGoToBridge'; -import SwapsController from '@metamask/swaps-controller'; +import SwapsController, { swapsUtils } from '@metamask/swaps-controller'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { getDecimalChainId } from '../../../util/networks'; +import { + getDecimalChainId, + isPortfolioViewEnabled, +} from '../../../util/networks'; import { useMetrics } from '../../../components/hooks/useMetrics'; import { createBuyNavigationDetails } from '../Ramp/routes/utils'; import { TokenI } from '../Tokens/types'; import AssetDetailsActions from '../../../components/Views/AssetDetails/AssetDetailsActions'; interface AssetOverviewProps { - navigation: { - navigate: (route: string, params: Record) => void; - }; asset: TokenI; displayBuyButton?: boolean; displaySwapsButton?: boolean; } const AssetOverview: React.FC = ({ - navigation, asset, displayBuyButton, displaySwapsButton, }: AssetOverviewProps) => { + const navigation = useNavigation(); const [timePeriod, setTimePeriod] = React.useState('1d'); - const currentCurrency = useSelector(selectCurrentCurrency); + const selectedInternalAccountAddress = useSelector( + selectSelectedInternalAccountAddress, + ); const conversionRate = useSelector(selectConversionRate); + const conversionRateByTicker = useSelector(selectCurrencyRates); + const currentCurrency = useSelector(selectCurrentCurrency); const accountsByChainId = useSelector(selectAccountsByChainId); const primaryCurrency = useSelector( (state: RootState) => state.settings.primaryCurrency, ); const goToBridge = useGoToBridge('TokenDetails'); const selectedAddress = useSelector( - selectSelectedInternalAccountChecksummedAddress, + selectSelectedInternalAccountFormattedAddress, ); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const tokenExchangeRates = useSelector(selectContractExchangeRates); + const allTokenMarketData = useSelector(selectTokenMarketData); const tokenBalances = useSelector(selectContractBalances); - const chainId = useSelector((state: RootState) => selectChainId(state)); - const ticker = useSelector((state: RootState) => selectTicker(state)); + const selectedChainId = useSelector((state: RootState) => + selectChainId(state), + ); + const selectedTicker = useSelector((state: RootState) => selectTicker(state)); + + const nativeCurrency = useSelector((state: RootState) => + selectNativeCurrencyByChainId(state, asset.chainId as Hex), + ); + + const multiChainTokenBalance = useSelector(selectTokensBalances); + const chainId = isPortfolioViewEnabled() + ? (asset.chainId as Hex) + : selectedChainId; + const ticker = isPortfolioViewEnabled() ? nativeCurrency : selectedTicker; + + let currentAddress: Hex; + + if (isPortfolioViewEnabled()) { + currentAddress = asset.address as Hex; + } else { + currentAddress = asset.isETH + ? getNativeTokenAddress(chainId as Hex) + : (asset.address as Hex); + } const { data: prices = [], isLoading } = useTokenHistoricalPrices({ - address: asset.isETH ? zeroAddress() : asset.address, + address: currentAddress, chainId, timePeriod, vsCurrency: currentCurrency, @@ -121,7 +161,41 @@ const AssetOverview: React.FC = ({ }); }; + const handleSwapNavigation = useCallback(() => { + navigation.navigate('Swaps', { + screen: 'SwapsAmountView', + params: { + sourceToken: asset.address ?? swapsUtils.NATIVE_SWAPS_TOKEN_ADDRESS, + sourcePage: 'MainView', + chainId: asset.chainId, + }, + }); + }, [navigation, asset.address, asset.chainId]); + const onSend = async () => { + if (isPortfolioViewEnabled()) { + navigation.navigate(Routes.WALLET.HOME, { + screen: Routes.WALLET.TAB_STACK_FLOW, + params: { + screen: Routes.WALLET_VIEW, + }, + }); + + if (asset.chainId !== selectedChainId) { + const { NetworkController } = Engine.context; + const networkConfiguration = + NetworkController.getNetworkConfigurationByChainId( + asset.chainId as Hex, + ); + + const networkClientId = + networkConfiguration?.rpcEndpoints?.[ + networkConfiguration.defaultRpcEndpointIndex + ]?.networkClientId; + + await NetworkController.setActiveNetwork(networkClientId as string); + } + } if (asset.isETH && ticker) { dispatch(newAssetTransaction(getEther(ticker))); } else { @@ -130,29 +204,74 @@ const AssetOverview: React.FC = ({ navigation.navigate('SendFlowView', {}); }; - const goToSwaps = () => { - navigation.navigate('Swaps', { - screen: 'SwapsAmountView', - params: { - sourceToken: asset.address, - sourcePage: 'MainView', - }, - }); - trackEvent(MetaMetricsEvents.SWAP_BUTTON_CLICKED, { - text: 'Swap', - tokenSymbol: '', - location: 'TokenDetails', - chain_id: getDecimalChainId(chainId), - }); - }; + const goToSwaps = useCallback(() => { + if (isPortfolioViewEnabled()) { + navigation.navigate(Routes.WALLET.HOME, { + screen: Routes.WALLET.TAB_STACK_FLOW, + params: { + screen: Routes.WALLET_VIEW, + }, + }); + if (asset.chainId !== selectedChainId) { + const { NetworkController } = Engine.context; + const networkConfiguration = + NetworkController.getNetworkConfigurationByChainId( + asset.chainId as Hex, + ); + + const networkClientId = + networkConfiguration?.rpcEndpoints?.[ + networkConfiguration.defaultRpcEndpointIndex + ]?.networkClientId; + + NetworkController.setActiveNetwork(networkClientId as string).then( + () => { + setTimeout(() => { + handleSwapNavigation(); + }, 500); + }, + ); + } else { + handleSwapNavigation(); + } + } else { + handleSwapNavigation(); + trackEvent( + createEventBuilder(MetaMetricsEvents.SWAP_BUTTON_CLICKED) + .addProperties({ + text: 'Swap', + tokenSymbol: '', + location: 'TokenDetails', + chain_id: getDecimalChainId(asset.chainId), + }) + .build(), + ); + } + }, [ + navigation, + asset.chainId, + selectedChainId, + trackEvent, + createEventBuilder, + handleSwapNavigation, + ]); + const onBuy = () => { - const [route, params] = createBuyNavigationDetails(); - navigation.navigate(route, params || {}); - trackEvent(MetaMetricsEvents.BUY_BUTTON_CLICKED, { - text: 'Buy', - location: 'TokenDetails', - chain_id_destination: getDecimalChainId(chainId), - }); + navigation.navigate( + ...createBuyNavigationDetails({ + address: asset.address, + chainId: getDecimalChainId(chainId), + }), + ); + trackEvent( + createEventBuilder(MetaMetricsEvents.BUY_BUTTON_CLICKED) + .addProperties({ + text: 'Buy', + location: 'TokenDetails', + chain_id_destination: getDecimalChainId(chainId), + }) + .build(), + ); }; const goToBrowserUrl = (url: string) => { @@ -199,14 +318,21 @@ const AssetOverview: React.FC = ({ )), [handleSelectTimePeriod, timePeriod], ); - const itemAddress = safeToChecksumAddress(asset.address); - const exchangeRate = itemAddress - ? tokenExchangeRates?.[itemAddress]?.price - : undefined; + + let exchangeRate: number | undefined; + if (!isPortfolioViewEnabled()) { + exchangeRate = itemAddress + ? tokenExchangeRates?.[itemAddress as Hex]?.price + : undefined; + } else { + const currentChainId = chainId as Hex; + exchangeRate = + allTokenMarketData?.[currentChainId]?.[itemAddress as Hex]?.price; + } let balance, balanceFiat; - if (asset.isETH) { + if (asset.isETH || asset.isNative) { balance = renderFromWei( //@ts-expect-error - This should be fixed at the accountsController selector level, ongoing discussion accountsByChainId[toHexadecimal(chainId)][selectedAddress]?.balance, @@ -220,9 +346,22 @@ const AssetOverview: React.FC = ({ currentCurrency, ); } else { + const multiChainTokenBalanceHex = + itemAddress && + multiChainTokenBalance?.[selectedInternalAccountAddress as Hex]?.[ + chainId as Hex + ]?.[itemAddress as Hex]; + + const selectedTokenBalanceHex = + itemAddress && tokenBalances?.[itemAddress as Hex]; + + const tokenBalanceHex = isPortfolioViewEnabled() + ? multiChainTokenBalanceHex + : selectedTokenBalanceHex; + balance = - itemAddress && tokenBalances?.[itemAddress] - ? renderFromTokenMinimalUnit(tokenBalances[itemAddress], asset.decimals) + itemAddress && tokenBalanceHex + ? renderFromTokenMinimalUnit(tokenBalanceHex, asset.decimals) : 0; balanceFiat = balanceToFiat( balance, @@ -233,23 +372,37 @@ const AssetOverview: React.FC = ({ } let mainBalance, secondaryBalance; - if (primaryCurrency === 'ETH') { - mainBalance = `${balance} ${asset.symbol}`; - secondaryBalance = balanceFiat; + if (!isPortfolioViewEnabled()) { + if (primaryCurrency === 'ETH') { + mainBalance = `${balance} ${asset.symbol}`; + secondaryBalance = balanceFiat; + } else { + mainBalance = !balanceFiat ? `${balance} ${asset.symbol}` : balanceFiat; + secondaryBalance = !balanceFiat + ? balanceFiat + : `${balance} ${asset.symbol}`; + } } else { - mainBalance = !balanceFiat ? `${balance} ${asset.symbol}` : balanceFiat; - secondaryBalance = !balanceFiat - ? balanceFiat - : `${balance} ${asset.symbol}`; + mainBalance = `${balance} ${asset.isETH ? asset.ticker : asset.symbol}`; + secondaryBalance = exchangeRate ? asset.balanceFiat : ''; } let currentPrice = 0; let priceDiff = 0; - if (asset.isETH) { - currentPrice = conversionRate || 0; - } else if (exchangeRate && conversionRate) { - currentPrice = exchangeRate * conversionRate; + if (!isPortfolioViewEnabled()) { + if (asset.isETH) { + currentPrice = conversionRate || 0; + } else if (exchangeRate && conversionRate) { + currentPrice = exchangeRate * conversionRate; + } + } else { + const tickerConversionRate = + conversionRateByTicker?.[nativeCurrency]?.conversionRate ?? 0; + currentPrice = + exchangeRate && tickerConversionRate + ? exchangeRate * tickerConversionRate + : 0; } const comparePrice = prices[0]?.[1] || 0; diff --git a/app/components/UI/AssetOverview/Balance/Balance.tsx b/app/components/UI/AssetOverview/Balance/Balance.tsx index fed53bd539a..82662417e44 100644 --- a/app/components/UI/AssetOverview/Balance/Balance.tsx +++ b/app/components/UI/AssetOverview/Balance/Balance.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { View } from 'react-native'; +import { Hex } from '@metamask/utils'; import { strings } from '../../../../../locales/i18n'; import { useStyles } from '../../../../component-library/hooks'; import styleSheet from './Balance.styles'; @@ -9,9 +10,11 @@ import { selectNetworkName } from '../../../../selectors/networkInfos'; import { selectChainId } from '../../../../selectors/networkController'; import { getTestNetImageByChainId, + getDefaultNetworkByChainId, isLineaMainnetByChainId, isMainnetByChainId, isTestNet, + isPortfolioViewEnabled, } from '../../../../util/networks'; import images from '../../../../images/image-icons'; import BadgeWrapper from '../../../../component-library/components/Badges/BadgeWrapper'; @@ -20,6 +23,7 @@ import Badge from '../../../../component-library/components/Badges/Badge/Badge'; import NetworkMainAssetLogo from '../../NetworkMainAssetLogo'; import AvatarToken from '../../../../component-library/components/Avatars/Avatar/variants/AvatarToken'; import { AvatarSize } from '../../../../component-library/components/Avatars/Avatar'; +import NetworkAssetLogo from '../../NetworkAssetLogo'; import Text, { TextVariant, } from '../../../../component-library/components/Texts/Text'; @@ -27,6 +31,11 @@ import { TokenI } from '../../Tokens/types'; import { useNavigation } from '@react-navigation/native'; import { isPooledStakingFeatureEnabled } from '../../Stake/constants'; import StakingBalance from '../../Stake/components/StakingBalance/StakingBalance'; +import { + PopularList, + UnpopularNetworkList, + CustomNetworkImgMapping, +} from '../../../../util/networks/customNetworks'; interface BalanceProps { asset: TokenI; @@ -34,17 +43,50 @@ interface BalanceProps { secondaryBalance?: string; } -export const NetworkBadgeSource = (chainId: string, ticker: string) => { +export const NetworkBadgeSource = (chainId: Hex, ticker: string) => { const isMainnet = isMainnetByChainId(chainId); const isLineaMainnet = isLineaMainnetByChainId(chainId); + if (!isPortfolioViewEnabled()) { + if (isTestNet(chainId)) return getTestNetImageByChainId(chainId); + if (isMainnet) return images.ETHEREUM; + + if (isLineaMainnet) return images['LINEA-MAINNET']; + + if (CustomNetworkImgMapping[chainId]) { + return CustomNetworkImgMapping[chainId]; + } + + return ticker ? images[ticker as keyof typeof images] : undefined; + } if (isTestNet(chainId)) return getTestNetImageByChainId(chainId); + const defaultNetwork = getDefaultNetworkByChainId(chainId) as + | { + imageSource: string; + } + | undefined; + + if (defaultNetwork) { + return defaultNetwork.imageSource; + } + + const unpopularNetwork = UnpopularNetworkList.find( + (networkConfig) => networkConfig.chainId === chainId, + ); - if (isMainnet) return images.ETHEREUM; + const customNetworkImg = CustomNetworkImgMapping[chainId]; - if (isLineaMainnet) return images['LINEA-MAINNET']; + const popularNetwork = PopularList.find( + (networkConfig) => networkConfig.chainId === chainId, + ); - return ticker ? images[ticker as keyof typeof images] : undefined; + const network = unpopularNetwork || popularNetwork; + if (network) { + return network.rpcPrefs.imageSource; + } + if (customNetworkImg) { + return customNetworkImg; + } }; const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { @@ -53,6 +95,44 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { const networkName = useSelector(selectNetworkName); const chainId = useSelector(selectChainId); + const tokenChainId = isPortfolioViewEnabled() ? asset.chainId : chainId; + + const ticker = asset.symbol; + + const renderNetworkAvatar = useCallback(() => { + if (!isPortfolioViewEnabled() && asset.isETH) { + return ; + } + + if (isPortfolioViewEnabled() && asset.isNative) { + return ( + + ); + } + + return ( + + ); + }, [ + asset.isETH, + asset.image, + asset.symbol, + asset.isNative, + asset.chainId, + styles.ethLogo, + ]); + return ( @@ -62,27 +142,26 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => { asset={asset} mainBalance={mainBalance} balance={secondaryBalance} - onPress={() => !asset.isETH && navigation.navigate('AssetDetails')} + onPress={() => + !asset.isETH && + !asset.isNative && + navigation.navigate('AssetDetails', { + chainId: asset.chainId, + address: asset.address, + }) + } > } > - {asset.isETH ? ( - - ) : ( - - )} + {renderNetworkAvatar()} {asset.name || asset.symbol} diff --git a/app/components/UI/AssetOverview/Balance/index.test.tsx b/app/components/UI/AssetOverview/Balance/index.test.tsx index 5607f71900a..ba5dd5f2fc3 100644 --- a/app/components/UI/AssetOverview/Balance/index.test.tsx +++ b/app/components/UI/AssetOverview/Balance/index.test.tsx @@ -7,6 +7,8 @@ import { selectChainId } from '../../../../selectors/networkController'; import { Provider, useSelector } from 'react-redux'; import configureMockStore from 'redux-mock-store'; import { backgroundState } from '../../../../util/test/initial-root-state'; +import { NetworkBadgeSource } from './Balance'; +import { isPortfolioViewEnabled } from '../../../../util/networks'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -35,6 +37,8 @@ const mockDAI = { symbol: 'DAI', isETH: false, logo: 'image-path', + chainId: '0x1', + isNative: false, }; const mockETH = { @@ -50,6 +54,8 @@ const mockETH = { symbol: 'ETH', isETH: true, logo: 'image-path', + chainId: '0x1', + isNative: true, }; const mockInitialState = { @@ -58,6 +64,16 @@ const mockInitialState = { }, }; +jest.mock('../../../../util/networks', () => ({ + ...jest.requireActual('../../../../util/networks'), + getTestNetImageByChainId: jest.fn((chainId) => `testnet-image-${chainId}`), +})); + +jest.mock('../../../../util/networks', () => ({ + ...jest.requireActual('../../../../util/networks'), + isPortfolioViewEnabled: jest.fn(), +})); + describe('Balance', () => { const mockStore = configureMockStore(); const store = mockStore(mockInitialState); @@ -83,23 +99,27 @@ describe('Balance', () => { jest.clearAllMocks(); }); - it('should render correctly with a fiat balance', () => { - const wrapper = render( - , - ); - expect(wrapper).toMatchSnapshot(); - }); + if (!isPortfolioViewEnabled()) { + it('should render correctly with a fiat balance', () => { + const wrapper = render( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + } - it('should render correctly without a fiat balance', () => { - const wrapper = render( - , - ); - expect(wrapper).toMatchSnapshot(); - }); + if (!isPortfolioViewEnabled()) { + it('should render correctly without a fiat balance', () => { + const wrapper = render( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + } it('should fire navigation event for non native tokens', () => { const { queryByTestId } = render( @@ -120,4 +140,62 @@ describe('Balance', () => { fireEvent.press(assetElement); expect(mockNavigate).toHaveBeenCalledTimes(0); }); + + describe('NetworkBadgeSource', () => { + it('returns testnet image for a testnet chainId', () => { + const result = NetworkBadgeSource('0xaa36a7', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns mainnet Ethereum image for mainnet chainId', () => { + const result = NetworkBadgeSource('0x1', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId', () => { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + }); + + it('returns undefined if no image is found', () => { + const result = NetworkBadgeSource('0x999', 'UNKNOWN'); + expect(result).toBeUndefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId isPortfolioViewEnabled is true', () => { + if (isPortfolioViewEnabled()) { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + } + }); + }); +}); + +describe('NetworkBadgeSource', () => { + it('returns testnet image for a testnet chainId', () => { + const result = NetworkBadgeSource('0xaa36a7', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns mainnet Ethereum image for mainnet chainId', () => { + const result = NetworkBadgeSource('0x1', 'ETH'); + expect(result).toBeDefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId', () => { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + }); + + it('returns undefined if no image is found', () => { + const result = NetworkBadgeSource('0x999', 'UNKNOWN'); + expect(result).toBeUndefined(); + }); + + it('returns Linea Mainnet image for Linea mainnet chainId isPortfolioViewEnabled is true', () => { + if (isPortfolioViewEnabled()) { + const result = NetworkBadgeSource('0xe708', 'LINEA'); + expect(result).toBeDefined(); + } + }); }); diff --git a/app/components/UI/AssetOverview/Price/Price.tsx b/app/components/UI/AssetOverview/Price/Price.tsx index 9d95b9e17b5..9e65e259d4c 100644 --- a/app/components/UI/AssetOverview/Price/Price.tsx +++ b/app/components/UI/AssetOverview/Price/Price.tsx @@ -17,7 +17,7 @@ import Text, { import PriceChart from '../PriceChart/PriceChart'; import { distributeDataPoints } from '../PriceChart/utils'; import styleSheet from './Price.styles'; -import { TokenOverviewSelectorsIDs } from '../../../../../e2e/selectors/TokenOverview.selectors'; +import { TokenOverviewSelectorsIDs } from '../../../../../e2e/selectors/wallet/TokenOverview.selectors'; import { TokenI } from '../../Tokens/types'; interface PriceProps { @@ -90,7 +90,10 @@ const Price = ({ {asset.symbol} )} {!isNaN(price) && ( - + {isLoading ? ( diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx index 7e8d341492d..881977207bb 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.test.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import { Hex } from '@metamask/utils'; +import { MarketDataDetails } from '@metamask/assets-controllers'; import renderWithProvider from '../../../../util/test/renderWithProvider'; import { backgroundState } from '../../../../util/test/initial-root-state'; import TokenDetails from './'; @@ -8,9 +10,12 @@ import { selectConversionRate, selectCurrentCurrency, } from '../../../../selectors/currencyRateController'; +import { + selectProviderConfig, + selectTicker, +} from '../../../../selectors/networkController'; // eslint-disable-next-line import/no-namespace import * as reactRedux from 'react-redux'; - jest.mock('../../../../core/Engine', () => ({ getTotalFiatAccountBalance: jest.fn(), context: { @@ -80,14 +85,60 @@ const mockContractExchangeRates = { }, }; +const mockTokenMarketDataByChainId: Record< + Hex, + Record +> = { + '0x1': { + '0x6B175474E89094C44Da98b954EedeAC495271d0F': { + allTimeHigh: 0.00045049491236145674, + allTimeLow: 0.00032567089582484455, + circulatingSupply: 5210102796.32321, + currency: 'ETH', + dilutedMarketCap: 1923097.9291743594, + high1d: 0.0003703658992610993, + low1d: 0.00036798603064620616, + marketCap: 1923097.9291743594, + marketCapPercentChange1d: -0.03026, + price: 0.00036902069191213795, + priceChange1d: 0.00134711, + pricePercentChange14d: -0.01961306580879152, + pricePercentChange1d: 0.13497913251736524, + pricePercentChange1h: -0.15571963819527113, + pricePercentChange1y: -0.01608509228365429, + pricePercentChange200d: -0.0287692372426721, + pricePercentChange30d: -0.08401729203937018, + pricePercentChange7d: 0.019578202262256407, + tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + totalVolume: 54440.464606773865, + }, + }, +}; + describe('TokenDetails', () => { beforeAll(() => { jest.resetAllMocks(); }); it('should render correctly', () => { const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector'); - useSelectorSpy.mockImplementation((selector) => { - switch (selector) { + useSelectorSpy.mockImplementation((selectorOrCallback) => { + const SELECTOR_MOCKS = { + selectTokenMarketDataByChainId: mockTokenMarketDataByChainId['0x1'], + selectConversionRateBySymbol: mockExchangeRate, + selectNativeCurrencyByChainId: 'ETH', + } as const; + + if (typeof selectorOrCallback === 'function') { + const selectorString = selectorOrCallback.toString(); + const matchedSelector = Object.keys(SELECTOR_MOCKS).find((key) => + selectorString.includes(key), + ); + if (matchedSelector) { + return SELECTOR_MOCKS[matchedSelector as keyof typeof SELECTOR_MOCKS]; + } + } + + switch (selectorOrCallback) { case selectTokenList: return mockAssets; case selectContractExchangeRates: @@ -133,10 +184,26 @@ describe('TokenDetails', () => { expect(toJSON()).toMatchSnapshot(); }); - it('should render TokenDetils without MarketDetails when marketData is null', () => { + it('should render Token Details without Market Details when marketData is null', () => { const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector'); - useSelectorSpy.mockImplementation((selector) => { - switch (selector) { + const SELECTOR_MOCKS = { + selectTokenMarketDataByChainId: {}, + selectConversionRateBySymbol: mockExchangeRate, + selectNativeCurrencyByChainId: 'ETH', + } as const; + + useSelectorSpy.mockImplementation((selectorOrCallback) => { + if (typeof selectorOrCallback === 'function') { + const selectorString = selectorOrCallback.toString(); + const matchedSelector = Object.keys(SELECTOR_MOCKS).find((key) => + selectorString.includes(key), + ); + if (matchedSelector) { + return SELECTOR_MOCKS[matchedSelector as keyof typeof SELECTOR_MOCKS]; + } + } + + switch (selectorOrCallback) { case selectTokenList: return mockAssets; case selectContractExchangeRates: @@ -145,6 +212,10 @@ describe('TokenDetails', () => { return mockExchangeRate; case selectCurrentCurrency: return mockCurrentCurrency; + case selectProviderConfig: + return { ticker: 'ETH' }; + case selectTicker: + return 'ETH'; default: return undefined; } @@ -162,8 +233,24 @@ describe('TokenDetails', () => { it('should render MarketDetails without TokenDetails when tokenList is null', () => { const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector'); - useSelectorSpy.mockImplementation((selector) => { - switch (selector) { + useSelectorSpy.mockImplementation((selectorOrCallback) => { + const SELECTOR_MOCKS = { + selectTokenMarketDataByChainId: mockTokenMarketDataByChainId['0x1'], + selectConversionRateBySymbol: mockExchangeRate, + selectNativeCurrencyByChainId: 'ETH', + } as const; + + if (typeof selectorOrCallback === 'function') { + const selectorString = selectorOrCallback.toString(); + const matchedSelector = Object.keys(SELECTOR_MOCKS).find((key) => + selectorString.includes(key), + ); + if (matchedSelector) { + return SELECTOR_MOCKS[matchedSelector as keyof typeof SELECTOR_MOCKS]; + } + } + + switch (selectorOrCallback) { case selectTokenList: return {}; case selectContractExchangeRates: @@ -179,9 +266,7 @@ describe('TokenDetails', () => { const { getByText, queryByText } = renderWithProvider( , - { - state: initialState, - }, + { state: initialState }, ); expect(queryByText('Token details')).toBeNull(); expect(getByText('Market details')).toBeDefined(); diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx index 368e2352d23..54df1781873 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx @@ -1,4 +1,6 @@ import { zeroAddress } from 'ethereumjs-util'; +import { Hex } from '@metamask/utils'; +import { RootState } from '../../../../reducers'; import React from 'react'; import { View } from 'react-native'; import { useSelector } from 'react-redux'; @@ -7,11 +9,16 @@ import { useStyles } from '../../../../component-library/hooks'; import styleSheet from './TokenDetails.styles'; import { safeToChecksumAddress } from '../../../../util/address'; import { selectTokenList } from '../../../../selectors/tokenListController'; -import { selectContractExchangeRates } from '../../../../selectors/tokenRatesController'; import { - selectConversionRate, + selectTokenMarketDataByChainId, + selectContractExchangeRates, +} from '../../../../selectors/tokenRatesController'; +import { + selectConversionRateBySymbol, selectCurrentCurrency, + selectConversionRate, } from '../../../../selectors/currencyRateController'; +import { selectNativeCurrencyByChainId } from '../../../../selectors/networkController'; import { convertDecimalToPercentage, localizeLargeNumber, @@ -23,6 +30,7 @@ import MarketDetailsList from './MarketDetailsList'; import { TokenI } from '../../Tokens/types'; import { isPooledStakingFeatureEnabled } from '../../Stake/constants'; import StakingEarnings from '../../Stake/components/StakingEarnings'; +import { isPortfolioViewEnabled } from '../../../../util/networks'; export interface TokenDetails { contractAddress: string | null; @@ -46,20 +54,36 @@ interface TokenDetailsProps { const TokenDetails: React.FC = ({ asset }) => { const { styles } = useStyles(styleSheet, {}); - const tokenList = useSelector(selectTokenList); - const tokenExchangeRates = useSelector(selectContractExchangeRates); - const conversionRate = useSelector(selectConversionRate); + const tokenExchangeRatesByChainId = useSelector((state: RootState) => + selectTokenMarketDataByChainId(state, asset.chainId as Hex), + ); + const nativeCurrency = useSelector((state: RootState) => + selectNativeCurrencyByChainId(state, asset.chainId as Hex), + ); + const tokenExchangeRatesLegacy = useSelector(selectContractExchangeRates); + const conversionRateLegacy = useSelector(selectConversionRate); + const conversionRateBySymbol = useSelector((state: RootState) => + selectConversionRateBySymbol(state, nativeCurrency), + ); const currentCurrency = useSelector(selectCurrentCurrency); const tokenContractAddress = safeToChecksumAddress(asset.address); + const tokenList = useSelector(selectTokenList); + + const conversionRate = isPortfolioViewEnabled() + ? conversionRateBySymbol + : conversionRateLegacy; + const tokenExchangeRates = isPortfolioViewEnabled() + ? tokenExchangeRatesByChainId + : tokenExchangeRatesLegacy; let tokenMetadata; let marketData; if (asset.isETH) { - marketData = tokenExchangeRates?.[zeroAddress() as `0x${string}`]; - } else if (!asset.isETH && tokenContractAddress) { + marketData = tokenExchangeRates?.[zeroAddress() as Hex]; + } else if (tokenContractAddress) { tokenMetadata = tokenList?.[tokenContractAddress.toLowerCase()]; - marketData = tokenExchangeRates?.[tokenContractAddress]; + marketData = tokenExchangeRates?.[tokenContractAddress as Hex]; } else { Logger.log('cannot find contract address'); return null; diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx index 1b0c37923d0..64f1e8da9d0 100644 --- a/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx +++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.test.tsx @@ -19,6 +19,7 @@ describe('TokenDetails', () => { beforeAll(() => { jest.resetAllMocks(); }); + it('should render correctly', () => { const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch'); useDispatchSpy.mockImplementation(() => jest.fn()); diff --git a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap index cc3cbd21a25..c1eba443d22 100644 --- a/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap +++ b/app/components/UI/AssetOverview/__snapshots__/AssetOverview.test.tsx.snap @@ -1133,3 +1133,1139 @@ exports[`AssetOverview should render correctly 1`] = ` `; + +exports[`AssetOverview should render correctly when portfolio view is enabled 1`] = ` + + + + + Ethereum + ( + ETH + ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1D + + + + + 1W + + + + + 1M + + + + + 3M + + + + + 1Y + + + + + 3Y + + + + + + + + + + + + + + + + Buy + + + + + + + + + + + + + + Swap + + + + + + + + + + + + + + Bridge + + + + + + + + + + + + + + Send + + + + + + + + + + + + + + Receive + + + + + + Your balance + + + + + + + + + + + + + + + + + + Ethereum + + + + 0 ETH + + + + + + + +`; diff --git a/app/components/UI/AssetSearch/index.test.tsx b/app/components/UI/AssetSearch/index.test.tsx index 9838088af3b..6009fe1393a 100644 --- a/app/components/UI/AssetSearch/index.test.tsx +++ b/app/components/UI/AssetSearch/index.test.tsx @@ -5,7 +5,7 @@ import { backgroundState } from '../../../util/test/initial-root-state'; import Engine from '../../../core/Engine'; const mockedEngine = Engine; -jest.mock('../../../core/Engine.ts', () => ({ +jest.mock('../../../core/Engine', () => ({ init: () => mockedEngine.init({}), context: { KeyringController: { diff --git a/app/components/UI/AssetSearch/index.tsx b/app/components/UI/AssetSearch/index.tsx index b1cfd4be105..3931f5c7c0e 100644 --- a/app/components/UI/AssetSearch/index.tsx +++ b/app/components/UI/AssetSearch/index.tsx @@ -14,7 +14,6 @@ import { useSelector } from 'react-redux'; import { TokenListToken } from '@metamask/assets-controllers'; import { useTheme } from '../../../util/theme'; import { ImportTokenViewSelectorsIDs } from '../../../../e2e/selectors/wallet/ImportTokenView.selectors'; -import { TokenViewSelectors } from '../../../../e2e/selectors/AddTokenView.selectors'; import { selectTokenListArray } from '../../../selectors/tokenListController'; import Icon, { IconName, @@ -140,7 +139,7 @@ const AssetSearch = memo(({ onSearch, onFocus, onBlur }: Props) => { return ( diff --git a/app/components/UI/BackupAlert/BackupAlert.tsx b/app/components/UI/BackupAlert/BackupAlert.tsx index 2e804b39993..134de7ca2df 100644 --- a/app/components/UI/BackupAlert/BackupAlert.tsx +++ b/app/components/UI/BackupAlert/BackupAlert.tsx @@ -8,7 +8,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { backUpSeedphraseAlertNotVisible } from '../../../actions/user'; import { findRouteNameFromNavigatorState } from '../../../util/general'; import { MetaMetricsEvents } from '../../../core/Analytics'; -import { ProtectWalletModalSelectorsIDs } from '../../../../e2e/selectors/Modals/ProtectWalletModal.selectors'; +import { ProtectWalletModalSelectorsIDs } from '../../../../e2e/selectors/Onboarding/ProtectWalletModal.selectors'; import styleSheet from './BackupAlert.styles'; import { useStyles } from '../../../component-library/hooks'; import { BackupAlertI } from './BackupAlert.types'; @@ -41,7 +41,7 @@ const BLOCKED_LIST = [ const BackupAlert = ({ navigation, onDismiss }: BackupAlertI) => { const { styles } = useStyles(styleSheet, {}); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const [inBrowserView, setInBrowserView] = useState(false); const [inBlockedView, setInBlockedView] = useState(false); const [isVisible, setIsVisible] = useState(true); @@ -77,19 +77,27 @@ const BackupAlert = ({ navigation, onDismiss }: BackupAlertI) => { screen: 'AccountBackupStep1', }); - trackEvent(MetaMetricsEvents.WALLET_SECURITY_PROTECT_ENGAGED, { - wallet_protection_required: false, - source: 'Backup Alert', - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_SECURITY_PROTECT_ENGAGED) + .addProperties({ + wallet_protection_required: false, + source: 'Backup Alert', + }) + .build(), + ); }; const onDismissAlert = () => { dispatch(backUpSeedphraseAlertNotVisible()); - trackEvent(MetaMetricsEvents.WALLET_SECURITY_PROTECT_DISMISSED, { - wallet_protection_required: false, - source: 'Backup Alert', - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_SECURITY_PROTECT_DISMISSED) + .addProperties({ + wallet_protection_required: false, + source: 'Backup Alert', + }) + .build(), + ); if (onDismiss) onDismiss(); }; diff --git a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx index c7181038c83..b488c94a26b 100644 --- a/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx +++ b/app/components/UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.tsx @@ -30,10 +30,8 @@ import NotificationsService from '../../../../util/notifications/services/Notifi import { MetaMetricsEvents } from '../../../../core/Analytics'; import { useEnableNotifications } from '../../../../util/notifications/hooks/useNotifications'; import { useMetrics } from '../../../hooks/useMetrics'; -import { - selectIsProfileSyncingEnabled, - selectIsMetamaskNotificationsEnabled, -} from '../../../../selectors/notifications'; +import { selectIsMetamaskNotificationsEnabled } from '../../../../selectors/notifications'; +import { selectIsProfileSyncingEnabled } from '../../../../selectors/identity'; interface Props { route: { @@ -44,7 +42,7 @@ interface Props { } const BasicFunctionalityModal = ({ route }: Props) => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const { colors } = useTheme(); const styles = createStyles(colors); const bottomSheetRef = useRef(null); @@ -65,27 +63,34 @@ const BasicFunctionalityModal = ({ route }: Props) => { if (permission !== 'authorized') { return; } - enableNotifications(); + enableNotifications(); }, [enableNotifications]); const closeBottomSheet = async () => { bottomSheetRef.current?.onCloseBottomSheet(() => { dispatch(toggleBasicFunctionality(!isEnabled)); trackEvent( - !isEnabled - ? MetaMetricsEvents.BASIC_FUNCTIONALITY_ENABLED - : MetaMetricsEvents.BASIC_FUNCTIONALITY_DISABLED, + createEventBuilder( + !isEnabled + ? MetaMetricsEvents.BASIC_FUNCTIONALITY_ENABLED + : MetaMetricsEvents.BASIC_FUNCTIONALITY_DISABLED, + ).build(), + ); + trackEvent( + createEventBuilder(MetaMetricsEvents.SETTINGS_UPDATED) + .addProperties({ + settings_group: 'security_privacy', + settings_type: 'basic_functionality', + old_value: isEnabled, + new_value: !isEnabled, + was_notifications_on: isEnabled + ? isNotificationsFeatureEnabled + : false, + was_profile_syncing_on: isEnabled ? isProfileSyncingEnabled : false, + }) + .build(), ); - trackEvent(MetaMetricsEvents.SETTINGS_UPDATED, { - settings_group: 'security_privacy', - settings_type: 'basic_functionality', - old_value: isEnabled, - new_value: !isEnabled, - was_notifications_on: isEnabled ? isNotificationsFeatureEnabled : false, - was_profile_syncing_on: isEnabled ? isProfileSyncingEnabled : false, - }); }); - if ( route.params.caller === Routes.SETTINGS.NOTIFICATIONS || route.params.caller === Routes.NOTIFICATIONS.OPT_IN diff --git a/app/components/UI/Bridge/utils/useGoToBridge.ts b/app/components/UI/Bridge/utils/useGoToBridge.ts index 0019ea22ca1..cc1d030b40b 100644 --- a/app/components/UI/Bridge/utils/useGoToBridge.ts +++ b/app/components/UI/Bridge/utils/useGoToBridge.ts @@ -23,7 +23,7 @@ export default function useGoToBridge(location: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const browserTabs = useSelector((state: any) => state.browser.tabs); const { navigate } = useNavigation(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); return (address?: string) => { const existingBridgeTab = browserTabs.find((tab: BrowserTab) => isBridgeUrl(tab.url), @@ -48,11 +48,15 @@ export default function useGoToBridge(location: string) { screen: Routes.BROWSER.VIEW, params, }); - trackEvent(MetaMetricsEvents.BRIDGE_LINK_CLICKED, { - bridgeUrl: AppConstants.BRIDGE.URL, - location, - chain_id_source: getDecimalChainId(chainId), - token_address_source: address, - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.BRIDGE_LINK_CLICKED) + .addProperties({ + bridgeUrl: AppConstants.BRIDGE.URL, + location, + chain_id_source: getDecimalChainId(chainId), + token_address_source: address, + }) + .build(), + ); }; } diff --git a/app/components/UI/BrowserBottomBar/index.js b/app/components/UI/BrowserBottomBar/index.js index 875bf6f93ac..d105fb8b26e 100644 --- a/app/components/UI/BrowserBottomBar/index.js +++ b/app/components/UI/BrowserBottomBar/index.js @@ -100,17 +100,27 @@ class BrowserBottomBar extends PureComponent { }; trackSearchEvent = () => { - this.props.metrics.trackEvent(MetaMetricsEvents.BROWSER_SEARCH_USED, { - option_chosen: 'Browser Bottom Bar Menu', - number_of_tabs: undefined, - }); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.BROWSER_SEARCH_USED) + .addProperties({ + option_chosen: 'Browser Bottom Bar Menu', + number_of_tabs: undefined, + }) + .build(), + ); }; trackNavigationEvent = (navigationOption) => { - this.props.metrics.trackEvent(MetaMetricsEvents.BROWSER_NAVIGATION, { - option_chosen: navigationOption, - os: Platform.OS, - }); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.BROWSER_NAVIGATION) + .addProperties({ + option_chosen: navigationOption, + os: Platform.OS, + }) + .build(), + ); }; render() { diff --git a/app/components/UI/CollectibleContractElement/index.js b/app/components/UI/CollectibleContractElement/index.js index 742d69dcdcd..25eddc35f18 100644 --- a/app/components/UI/CollectibleContractElement/index.js +++ b/app/components/UI/CollectibleContractElement/index.js @@ -13,7 +13,7 @@ import { removeFavoriteCollectible } from '../../../actions/collectibles'; import { collectibleContractsSelector } from '../../../reducers/collectibles'; import { useTheme } from '../../../util/theme'; import { selectChainId } from '../../../selectors/networkController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; import Icon, { IconName, IconColor, @@ -106,7 +106,7 @@ function CollectibleContractElement({ const longPressedCollectible = useRef(null); const { colors, themeAppearance, brandColors } = useTheme(); const styles = createStyles(colors, brandColors); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const toggleCollectibles = useCallback(() => { setCollectiblesVisible(!collectiblesVisible); @@ -138,9 +138,13 @@ function CollectibleContractElement({ longPressedCollectible.current.address, longPressedCollectible.current.tokenId, ); - trackEvent(MetaMetricsEvents.COLLECTIBLE_REMOVED, { - chain_id: getDecimalChainId(chainId), - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.COLLECTIBLE_REMOVED) + .addProperties({ + chain_id: getDecimalChainId(chainId), + }) + .build(), + ); Alert.alert( strings('wallet.collectible_removed_title'), strings('wallet.collectible_removed_desc'), @@ -316,7 +320,7 @@ CollectibleContractElement.propTypes = { const mapStateToProps = (state) => ({ collectibleContracts: collectibleContractsSelector(state), chainId: selectChainId(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/app/components/UI/CollectibleContractOverview/index.js b/app/components/UI/CollectibleContractOverview/index.js index ce41eacbb26..94942c48f33 100644 --- a/app/components/UI/CollectibleContractOverview/index.js +++ b/app/components/UI/CollectibleContractOverview/index.js @@ -13,7 +13,7 @@ import { newAssetTransaction } from '../../../actions/transaction'; import { toLowerCaseEquals } from '../../../util/general'; import { collectiblesSelector } from '../../../reducers/collectibles'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import { SEND_BUTTON_ID } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; +import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/wallet/TokenOverview.selectors'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; const createStyles = (colors) => @@ -139,7 +139,7 @@ class CollectibleContractOverview extends PureComponent { icon="send" onPress={this.onSend} label={leftActionButtonText} - testID={SEND_BUTTON_ID} + testID={TokenOverviewSelectorsIDs.SEND_BUTTON} /> singleCollectible.isCurrentlyOwned === true, ); const { colors } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const styles = createStyles(colors); const [isAddNFTEnabled, setIsAddNFTEnabled] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -218,9 +218,11 @@ const CollectibleContracts = ({ const goToAddCollectible = useCallback(() => { setIsAddNFTEnabled(false); navigation.push('AddAsset', { assetType: 'collectible' }); - trackEvent(MetaMetricsEvents.WALLET_ADD_COLLECTIBLES); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_ADD_COLLECTIBLES).build(), + ); setIsAddNFTEnabled(true); - }, [navigation, trackEvent]); + }, [navigation, trackEvent, createEventBuilder]); const renderFooter = useCallback( () => ( @@ -439,7 +441,7 @@ CollectibleContracts.propTypes = { const mapStateToProps = (state) => ({ networkType: selectProviderType(state), chainId: selectChainId(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), useNftDetection: selectUseNftDetection(state), collectibleContracts: collectibleContractsSelector(state), collectibles: collectiblesSelector(state), diff --git a/app/components/UI/CollectibleModal/CollectibleModal.tsx b/app/components/UI/CollectibleModal/CollectibleModal.tsx index 118aa84f13f..df135232abc 100644 --- a/app/components/UI/CollectibleModal/CollectibleModal.tsx +++ b/app/components/UI/CollectibleModal/CollectibleModal.tsx @@ -33,7 +33,7 @@ import { EXTERNAL_LINK_TYPE } from '../../../constants/browser'; const CollectibleModal = () => { const navigation = useNavigation(); const dispatch = useDispatch(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const chainId = useSelector(selectChainId); const { contractName, collectible } = useParams(); @@ -68,9 +68,11 @@ const CollectibleModal = () => { }, [handleUpdateCollectible]); useEffect(() => { - trackEvent(MetaMetricsEvents.COLLECTIBLE_DETAILS_OPENED, { - chain_id: getDecimalChainId(chainId), - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.COLLECTIBLE_DETAILS_OPENED) + .addProperties({ chain_id: getDecimalChainId(chainId) }) + .build(), + ); // The linter wants `trackEvent` to be added as a dependency, // But the event fires twice if I do that. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/app/components/UI/CollectibleOverview/index.js b/app/components/UI/CollectibleOverview/index.js index a2015051686..33999cfb2dc 100644 --- a/app/components/UI/CollectibleOverview/index.js +++ b/app/components/UI/CollectibleOverview/index.js @@ -48,7 +48,7 @@ import { selectDisplayNftMedia, selectIsIpfsGatewayEnabled, } from '../../../selectors/preferencesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; const ANIMATION_VELOCITY = 250; const HAS_NOTCH = Device.hasNotch(); @@ -546,7 +546,7 @@ CollectibleOverview.propTypes = { const mapStateToProps = (state, props) => ({ chainId: selectChainId(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), isInFavorites: isCollectibleInFavoritesSelector(state, props.collectible), }); diff --git a/app/components/UI/DeleteWalletModal/index.tsx b/app/components/UI/DeleteWalletModal/index.tsx index 843fad7db96..fb12f474eca 100644 --- a/app/components/UI/DeleteWalletModal/index.tsx +++ b/app/components/UI/DeleteWalletModal/index.tsx @@ -38,7 +38,7 @@ if (Device.isAndroid() && UIManager.setLayoutAnimationEnabledExperimental) { const DeleteWalletModal = () => { const navigation = useNavigation(); const { colors, themeAppearance } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const styles = createStyles(colors); const modalRef = useRef(null); @@ -94,7 +94,11 @@ const DeleteWalletModal = () => { triggerClose(); await resetWalletState(); await deleteUser(); - trackEvent(MetaMetricsEvents.DELETE_WALLET_MODAL_WALLET_DELETED, {}); + trackEvent( + createEventBuilder( + MetaMetricsEvents.DELETE_WALLET_MODAL_WALLET_DELETED, + ).build(), + ); InteractionManager.runAfterInteractions(() => { navigateOnboardingRoot(); }); diff --git a/app/components/UI/DeprecatedNetworkModal/DeprecatedNetworkModal.tsx b/app/components/UI/DeprecatedNetworkModal/DeprecatedNetworkModal.tsx index e1917cade59..ee5a2b11058 100644 --- a/app/components/UI/DeprecatedNetworkModal/DeprecatedNetworkModal.tsx +++ b/app/components/UI/DeprecatedNetworkModal/DeprecatedNetworkModal.tsx @@ -19,7 +19,7 @@ import { MetaMetricsEvents } from '../../../core/Analytics'; const DeprecatedNetworkModal = () => { const { styles } = useStyles(styleSheet, {}); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const navigation = useNavigation(); const dismissModal = (): void => { @@ -28,11 +28,15 @@ const DeprecatedNetworkModal = () => { const goToLearnMore = () => { Linking.openURL(CONNECTING_TO_DEPRECATED_NETWORK); - trackEvent(MetaMetricsEvents.EXTERNAL_LINK_CLICKED, { - location: 'dapp_connection_request', - text: 'Learn More', - url_domain: CONNECTING_TO_DEPRECATED_NETWORK, - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.EXTERNAL_LINK_CLICKED) + .addProperties({ + location: 'dapp_connection_request', + text: 'Learn More', + url_domain: CONNECTING_TO_DEPRECATED_NETWORK, + }) + .build(), + ); }; const sheetRef = useRef(null); diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index d3aea2d1ba5..618451f6626 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -539,11 +539,15 @@ class DrawerView extends PureComponent { this.setState({ showProtectWalletModal: true }); this.props.metrics.trackEvent( - MetaMetricsEvents.WALLET_SECURITY_PROTECT_VIEWED, - { - wallet_protection_required: false, - source: 'Backup Alert', - }, + this.props.metrics + .createEventBuilder( + MetaMetricsEvents.WALLET_SECURITY_PROTECT_VIEWED, + ) + .addProperties({ + wallet_protection_required: false, + source: 'Backup Alert', + }) + .build(), ); } else { // eslint-disable-next-line react/no-did-update-set-state @@ -602,20 +606,25 @@ class DrawerView extends PureComponent { onSelectAccount: this.hideDrawer, }), ); - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_ACCOUNT_NAME); - }; - - trackEvent = (event) => { - this.props.metrics.trackEvent(event); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_ACCOUNT_NAME) + .build(), + ); }; // NOTE: do we need this event? trackOpenBrowserEvent = () => { const { providerConfig } = this.props; - this.props.metrics.trackEvent(MetaMetricsEvents.BROWSER_OPENED, { - source: 'In-app Navigation', - chain_id: getDecimalChainId(providerConfig.chainId), - }); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.BROWSER_OPENED) + .addProperties({ + source: 'In-app Navigation', + chain_id: getDecimalChainId(providerConfig.chainId), + }) + .build(), + ); }; onReceive = () => { @@ -623,14 +632,22 @@ class DrawerView extends PureComponent { initialScreen: QRTabSwitcherScreens.Receive, disableTabber: true, }); - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_RECEIVE); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_RECEIVE) + .build(), + ); }; onSend = async () => { this.props.newAssetTransaction(getEther(this.props.ticker)); this.props.navigation.navigate('SendFlowView'); this.hideDrawer(); - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_SEND); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_SEND) + .build(), + ); }; goToBrowser = () => { @@ -638,13 +655,21 @@ class DrawerView extends PureComponent { this.hideDrawer(); // Q: duplicated analytic event? this.trackOpenBrowserEvent(); - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_BROWSER); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_BROWSER) + .build(), + ); }; showWallet = () => { this.props.navigation.navigate('WalletTabHome'); this.hideDrawer(); - this.trackEvent(MetaMetricsEvents.WALLET_OPENED); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.WALLET_OPENED) + .build(), + ); }; onPressLock = async () => { @@ -677,7 +702,11 @@ class DrawerView extends PureComponent { ], { cancelable: false }, ); - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_LOGOUT); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_LOGOUT) + .build(), + ); }; viewInEtherscan = () => { @@ -701,11 +730,19 @@ class DrawerView extends PureComponent { ); this.goToBrowserUrl(url, etherscan_url); } - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_VIEW_ETHERSCAN); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_VIEW_ETHERSCAN) + .build(), + ); }; submitFeedback = () => { - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_SEND_FEEDBACK); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_SEND_FEEDBACK) + .build(), + ); this.goToBrowserUrl( 'https://community.metamask.io/c/feature-requests-ideas/', strings('drawer.request_feature'), @@ -720,7 +757,11 @@ class DrawerView extends PureComponent { timestamp: Date.now(), }, }); - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_GET_HELP); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NAVIGATION_TAPS_GET_HELP) + .build(), + ); this.hideDrawer(); }; @@ -900,7 +941,13 @@ class DrawerView extends PureComponent { .catch((err) => { Logger.log('Error while trying to share address', err); }); - this.trackEvent(MetaMetricsEvents.NAVIGATION_TAPS_SHARE_PUBLIC_ADDRESS); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder( + MetaMetricsEvents.NAVIGATION_TAPS_SHARE_PUBLIC_ADDRESS, + ) + .build(), + ); }; onSecureWalletModalAction = () => { @@ -911,11 +958,13 @@ class DrawerView extends PureComponent { ); InteractionManager.runAfterInteractions(() => { this.props.metrics.trackEvent( - MetaMetricsEvents.WALLET_SECURITY_PROTECT_ENGAGED, - { - wallet_protection_required: true, - source: 'Modal', - }, + this.props.metrics + .createEventBuilder(MetaMetricsEvents.WALLET_SECURITY_PROTECT_ENGAGED) + .addProperties({ + wallet_protection_required: true, + source: 'Modal', + }) + .build(), ); }); }; diff --git a/app/components/UI/EditGasFee1559/__snapshots__/index.test.tsx.snap b/app/components/UI/EditGasFee1559/__snapshots__/index.test.tsx.snap index 9e57dc2ffc7..d1dd983557a 100644 --- a/app/components/UI/EditGasFee1559/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/EditGasFee1559/__snapshots__/index.test.tsx.snap @@ -135,7 +135,40 @@ exports[`EditGasFee1559 should render correctly 1`] = ` + Low + , + "name": "low", + "topLabel": false, + }, + { + "label": + Market + , + "name": "medium", + "topLabel": false, + }, + { + "label": + Aggressive + , + "name": "high", + "topLabel": false, + }, + ] + } /> { if (!showAdvancedOptions) { trackEvent( - MetaMetricsEvents.GAS_ADVANCED_OPTIONS_CLICKED, - getAnalyticsParams(), + createEventBuilder(MetaMetricsEvents.GAS_ADVANCED_OPTIONS_CLICKED) + .addProperties(getAnalyticsParams()) + .build(), ); } setShowAdvancedOptions((showAdvancedOptions) => !showAdvancedOptions); @@ -231,7 +232,11 @@ const EditGasFee1559 = ({ }; const save = () => { - trackEvent(MetaMetricsEvents.GAS_FEE_CHANGED, getAnalyticsParams()); + trackEvent( + createEventBuilder(MetaMetricsEvents.GAS_FEE_CHANGED) + .addProperties(getAnalyticsParams()) + .build(), + ); onSave(selectedOption); }; @@ -393,7 +398,7 @@ const EditGasFee1559 = ({ diff --git a/app/components/UI/EditGasFeeLegacy/index.js b/app/components/UI/EditGasFeeLegacy/index.js index 34681b6326b..dac72d02942 100644 --- a/app/components/UI/EditGasFeeLegacy/index.js +++ b/app/components/UI/EditGasFeeLegacy/index.js @@ -148,7 +148,7 @@ const EditGasFeeLegacy = ({ const [selectedOption, setSelectedOption] = useState(selected); const [gasPriceError, setGasPriceError] = useState(); const { colors } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const styles = createStyles(colors); const getAnalyticsParams = () => { @@ -168,15 +168,20 @@ const EditGasFeeLegacy = ({ const toggleAdvancedOptions = () => { if (!showAdvancedOptions) { trackEvent( - MetaMetricsEvents.GAS_ADVANCED_OPTIONS_CLICKED, - getAnalyticsParams(), + createEventBuilder(MetaMetricsEvents.GAS_ADVANCED_OPTIONS_CLICKED) + .addProperties(getAnalyticsParams()) + .build(), ); } setShowAdvancedOptions((showAdvancedOptions) => !showAdvancedOptions); }; const save = () => { - trackEvent(MetaMetricsEvents.GAS_FEE_CHANGED, getAnalyticsParams()); + trackEvent( + createEventBuilder(MetaMetricsEvents.GAS_FEE_CHANGED) + .addProperties(getAnalyticsParams()) + .build(), + ); onSave(selectedOption); }; diff --git a/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx b/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx index e729f72f7e1..267717fe297 100644 --- a/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx +++ b/app/components/UI/EnableAutomaticSecurityChecksModal/EnableAutomaticSecurityChecksModal.tsx @@ -23,7 +23,7 @@ import { import { MetaMetricsEvents } from '../../../core/Analytics'; import { ScrollView } from 'react-native-gesture-handler'; -import { EnableAutomaticSecurityChecksIDs } from '../../../../e2e/selectors/Modals/EnableAutomaticSecurityChecks.selectors'; +import { EnableAutomaticSecurityChecksIDs } from '../../../../e2e/selectors/Onboarding/EnableAutomaticSecurityChecks.selectors'; import generateDeviceAnalyticsMetaData from '../../../util/metrics'; import { useMetrics } from '../../../components/hooks/useMetrics'; @@ -38,7 +38,7 @@ export const createEnableAutomaticSecurityChecksModalNavDetails = const EnableAutomaticSecurityChecksModal = () => { const { colors } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const styles = createStyles(colors); const modalRef = useRef(null); const dispatch = useDispatch(); @@ -47,10 +47,14 @@ const EnableAutomaticSecurityChecksModal = () => { modalRef?.current?.dismissModal(cb); useEffect(() => { - trackEvent(MetaMetricsEvents.AUTOMATIC_SECURITY_CHECKS_PROMPT_VIEWED, { - ...generateDeviceAnalyticsMetaData(), - }); - }, [trackEvent]); + trackEvent( + createEventBuilder( + MetaMetricsEvents.AUTOMATIC_SECURITY_CHECKS_PROMPT_VIEWED, + ) + .addProperties(generateDeviceAnalyticsMetaData()) + .build(), + ); + }, [trackEvent, createEventBuilder]); useEffect(() => { dispatch(setAutomaticSecurityChecksModalOpen(true)); @@ -63,24 +67,30 @@ const EnableAutomaticSecurityChecksModal = () => { () => dismissModal(() => { trackEvent( - MetaMetricsEvents.AUTOMATIC_SECURITY_CHECKS_DISABLED_FROM_PROMPT, - { ...generateDeviceAnalyticsMetaData() }, + createEventBuilder( + MetaMetricsEvents.AUTOMATIC_SECURITY_CHECKS_DISABLED_FROM_PROMPT, + ) + .addProperties(generateDeviceAnalyticsMetaData()) + .build(), ); dispatch(userSelectedAutomaticSecurityChecksOptions()); }), - [dispatch, trackEvent], + [dispatch, trackEvent, createEventBuilder], ); const enableAutomaticSecurityChecks = useCallback(() => { dismissModal(() => { trackEvent( - MetaMetricsEvents.AUTOMATIC_SECURITY_CHECKS_ENABLED_FROM_PROMPT, - { ...generateDeviceAnalyticsMetaData() }, + createEventBuilder( + MetaMetricsEvents.AUTOMATIC_SECURITY_CHECKS_ENABLED_FROM_PROMPT, + ) + .addProperties(generateDeviceAnalyticsMetaData()) + .build(), ); dispatch(userSelectedAutomaticSecurityChecksOptions()); dispatch(setAutomaticSecurityChecks(true)); }); - }, [dispatch, trackEvent]); + }, [dispatch, trackEvent, createEventBuilder]); return ( diff --git a/app/components/UI/Identicon/index.tsx b/app/components/UI/Identicon/index.tsx index d4dfffd2d17..da74af1f3f3 100644 --- a/app/components/UI/Identicon/index.tsx +++ b/app/components/UI/Identicon/index.tsx @@ -47,7 +47,8 @@ const Identicon: React.FC = ({ const { colors } = useTheme(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const useBlockieIcon = useSelector((state: RootState) => state.settings.useBlockieIcon) ?? true; + const useBlockieIcon = + useSelector((state: RootState) => state.settings.useBlockieIcon) ?? true; if (!address && !imageUri) return null; diff --git a/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx b/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx index 165c4032f0f..9fc0190d1f6 100644 --- a/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx +++ b/app/components/UI/LedgerModals/LedgerConfirmationModal.test.tsx @@ -20,6 +20,7 @@ import { useMetrics } from '../../hooks/useMetrics'; import { MetaMetricsEvents } from '../../../core/Analytics'; import { fireEvent } from '@testing-library/react-native'; import { HardwareDeviceTypes } from '../../../constants/keyringTypes'; +import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder'; jest.mock('../../hooks/Ledger/useBluetooth', () => ({ __esModule: true, @@ -43,9 +44,9 @@ jest.mock('@react-navigation/native', () => ({ }), })); -jest.mock('../../../components/hooks/useMetrics', () => ({ - useMetrics: jest.fn(), -})); +jest.mock('../../../components/hooks/useMetrics'); + +const mockTrackEvent = jest.fn(); describe('LedgerConfirmationModal', () => { beforeEach(() => { @@ -70,8 +71,18 @@ describe('LedgerConfirmationModal', () => { error: null, }); - (useMetrics as jest.Mock).mockReturnValue({ - trackEvent: jest.fn(), + (useMetrics as jest.MockedFn).mockReturnValue({ + trackEvent: mockTrackEvent, + createEventBuilder: MetricsEventBuilder.createEventBuilder, + enable: jest.fn(), + addTraitsToUser: jest.fn(), + createDataDeletionTask: jest.fn(), + checkDataDeleteStatus: jest.fn(), + getDeleteRegulationCreationDate: jest.fn(), + getDeleteRegulationId: jest.fn(), + isDataRecorded: jest.fn(), + isEnabled: jest.fn(), + getMetaMetricsId: jest.fn(), }); }); @@ -378,11 +389,6 @@ describe('LedgerConfirmationModal', () => { throw new Error('error'); }); - const trackEvent = jest.fn(); - (useMetrics as jest.Mock).mockReturnValue({ - trackEvent, - }); - renderWithProvider( { expect(onConfirmation).not.toHaveBeenCalled(); - expect(trackEvent).toHaveBeenNthCalledWith( + expect(mockTrackEvent).toHaveBeenNthCalledWith( 1, - MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, - { - device_type: HardwareDeviceTypes.LEDGER, - error: 'LEDGER_ETH_APP_NOT_INSTALLED', - }, + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, + ) + .addProperties({ + device_type: HardwareDeviceTypes.LEDGER, + error: 'LEDGER_ETH_APP_NOT_INSTALLED', + }) + .build(), ); }); diff --git a/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx b/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx index b35967f8c40..33b7f55414b 100644 --- a/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx +++ b/app/components/UI/LedgerModals/LedgerConfirmationModal.tsx @@ -46,7 +46,7 @@ const LedgerConfirmationModal = ({ }: LedgerConfirmationModalProps) => { const { colors } = useAppThemeFromContext() || mockTheme; const styles = useMemo(() => createStyles(colors), [colors]); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const [delayClose, setDelayClose] = useState(false); const [completeClose, setCompleteClose] = useState(false); const [permissionErrorShown, setPermissionErrorShown] = useState(false); @@ -78,10 +78,14 @@ const LedgerConfirmationModal = ({ } catch (_e) { // Handle a super edge case of the user starting a transaction with the device connected // After arriving to confirmation the ETH app is not installed anymore this causes a crash. - trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: HardwareDeviceTypes.LEDGER, - error: 'LEDGER_ETH_APP_NOT_INSTALLED', - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR) + .addProperties({ + device_type: HardwareDeviceTypes.LEDGER, + error: 'LEDGER_ETH_APP_NOT_INSTALLED', + }) + .build(), + ); } }; @@ -90,9 +94,15 @@ const LedgerConfirmationModal = ({ try { onRejection(); } finally { - trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_TRANSACTION_CANCELLED, { - device_type: HardwareDeviceTypes.LEDGER, - }); + trackEvent( + createEventBuilder( + MetaMetricsEvents.LEDGER_HARDWARE_TRANSACTION_CANCELLED, + ) + .addProperties({ + device_type: HardwareDeviceTypes.LEDGER, + }) + .build(), + ); } }; @@ -198,10 +208,14 @@ const LedgerConfirmationModal = ({ break; } if (ledgerError !== LedgerCommunicationErrors.UserRefusedConfirmation) { - trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: HardwareDeviceTypes.LEDGER, - error: `${ledgerError}`, - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR) + .addProperties({ + device_type: HardwareDeviceTypes.LEDGER, + error: `${ledgerError}`, + }) + .build(), + ); } } @@ -227,10 +241,14 @@ const LedgerConfirmationModal = ({ break; } setPermissionErrorShown(true); - trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: HardwareDeviceTypes.LEDGER, - error: 'LEDGER_BLUETOOTH_PERMISSION_ERR', - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR) + .addProperties({ + device_type: HardwareDeviceTypes.LEDGER, + error: 'LEDGER_BLUETOOTH_PERMISSION_ERR', + }) + .build(), + ); } if (bluetoothConnectionError) { @@ -238,10 +256,15 @@ const LedgerConfirmationModal = ({ title: strings('ledger.bluetooth_off'), subtitle: strings('ledger.bluetooth_off_message'), }); - trackEvent(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR, { - device_type: HardwareDeviceTypes.LEDGER, - error: 'LEDGER_BLUETOOTH_CONNECTION_ERR', - }); + + trackEvent( + createEventBuilder(MetaMetricsEvents.LEDGER_HARDWARE_WALLET_ERROR) + .addProperties({ + device_type: HardwareDeviceTypes.LEDGER, + error: 'LEDGER_BLUETOOTH_CONNECTION_ERR', + }) + .build(), + ); } if ( diff --git a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx index 3deb0c47837..69012308952 100644 --- a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx +++ b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx @@ -3,9 +3,8 @@ import { Switch, Text, View } from 'react-native'; import { strings } from '../../../../locales/i18n'; import { BIOMETRY_TYPE } from 'react-native-keychain'; import { createStyles } from './styles'; -import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selectors'; +import { LoginViewSelectors } from '../../../../e2e/selectors/wallet/LoginView.selectors'; import { useSelector } from 'react-redux'; -import { LoginOptionsSwitchSelectorsIDs } from '../../../../e2e/selectors/LoginOptionsSwitch.selectors'; import { useTheme } from '../../../util/theme'; interface Props { @@ -69,7 +68,6 @@ const LoginOptionsSwitch = ({ }} thumbColor={theme.brandColors.white} ios_backgroundColor={colors.border.muted} - testID={LoginOptionsSwitchSelectorsIDs.BIOMETRICS_SWITCH} /> ); diff --git a/app/components/UI/ManageNetworks/ManageNetworks.tsx b/app/components/UI/ManageNetworks/ManageNetworks.tsx index 56fed1d3a22..97a721291fa 100644 --- a/app/components/UI/ManageNetworks/ManageNetworks.tsx +++ b/app/components/UI/ManageNetworks/ManageNetworks.tsx @@ -27,7 +27,7 @@ import styles from './ManageNetworks.styles'; export default function ManageNetworksComponent() { const providerConfig: ProviderConfig = useSelector(selectProviderConfig); const navigation = useNavigation(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const networkImageSource = useSelector(selectNetworkImageSource); const networkName = useSelector(selectNetworkName); @@ -37,10 +37,14 @@ export default function ManageNetworksComponent() { screen: Routes.SHEET.NETWORK_SELECTOR, }); - trackEvent(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED, { - chain_id: getDecimalChainId(providerConfig.chainId), - }); - }, [navigation, trackEvent, providerConfig]); + trackEvent( + createEventBuilder(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED) + .addProperties({ + chain_id: getDecimalChainId(providerConfig.chainId), + }) + .build(), + ); + }, [navigation, trackEvent, providerConfig, createEventBuilder]); const handleLink = () => { Linking.openURL(AppConstants.URLS.PRIVACY_POLICY_2024); diff --git a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap index 3aef46b9799..c5e91ce27bf 100644 --- a/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap +++ b/app/components/UI/ManageNetworks/__snapshots__/ManageNetworks.test.js.snap @@ -68,6 +68,7 @@ exports[`ManageNetworks should render correctly 1`] = ` to learn more about how Infura handles data. { - MetaMetrics.getInstance().trackEvent(event, params); + MetaMetrics.getInstance().trackEvent(event); }; const styles = StyleSheet.create({ @@ -218,7 +223,10 @@ export function getNavigationOptionsTitle( }); function navigationPop() { - if (navigationPopEvent) trackEvent(navigationPopEvent); + if (navigationPopEvent) + trackEvent( + MetricsEventBuilder.createEventBuilder(navigationPopEvent).build(), + ); navigation.goBack(); } @@ -555,11 +563,15 @@ export function getSendFlowTitle( const providerType = route?.params?.providerType ?? ''; const additionalTransactionMetricsParams = getBlockaidTransactionMetricsParams(transaction); - trackEvent(MetaMetricsEvents.SEND_FLOW_CANCEL, { - view: title.split('.')[1], - network: providerType, - ...additionalTransactionMetricsParams, - }); + trackEvent( + MetricsEventBuilder.createEventBuilder(MetaMetricsEvents.SEND_FLOW_CANCEL) + .addProperties({ + view: title.split('.')[1], + network: providerType, + ...additionalTransactionMetricsParams, + }) + .build(), + ); resetTransaction(); navigation.dangerouslyGetParent()?.pop(); }; @@ -910,7 +922,7 @@ export function getOfflineModalNavbar() { * Function that returns the navigation options for the wallet screen. * * @param {Object} accountActionsRef - The ref object for the account actions - * @param {string} selectedAddress - The currently selected Ethereum address + * @param {Object} selectedInternalAccount - The currently selected internal account * @param {string} accountName - The name of the currently selected account * @param {string} accountAvatarType - The type of avatar for the currently selected account * @param {string} networkName - The name of the current network @@ -926,7 +938,7 @@ export function getOfflineModalNavbar() { */ export function getWalletNavbarOptions( accountActionsRef, - selectedAddress, + selectedInternalAccount, accountName, accountAvatarType, networkName, @@ -955,6 +967,15 @@ export function getWalletNavbarOptions( }, }); + let formattedAddress = toChecksumHexAddress(selectedInternalAccount.address); + + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + if (isBtcAccount(selectedInternalAccount)) { + // BTC addresses are not checksummed + formattedAddress = selectedInternalAccount.address; + } + ///: END:ONLY_INCLUDE_IF + const onScanSuccess = (data, content) => { if (data.private_key) { Alert.alert( @@ -1003,54 +1024,90 @@ export function getWalletNavbarOptions( navigation.navigate(Routes.QR_TAB_SWITCHER, { onScanSuccess, }); - trackEvent(MetaMetricsEvents.WALLET_QR_SCANNER); + trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.WALLET_QR_SCANNER, + ).build(), + ); } function handleNotificationOnPress() { if (isNotificationEnabled && isNotificationsFeatureEnabled()) { navigation.navigate(Routes.NOTIFICATIONS.VIEW); - trackEvent(MetaMetricsEvents.NOTIFICATIONS_MENU_OPENED, { - unread_count: unreadNotificationCount, - read_count: readNotificationCount, - }); + trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.NOTIFICATIONS_MENU_OPENED, + ) + .addProperties({ + unread_count: unreadNotificationCount, + read_count: readNotificationCount, + }) + .build(), + ); } else { navigation.navigate(Routes.NOTIFICATIONS.OPT_IN_STACK); - trackEvent(MetaMetricsEvents.NOTIFICATIONS_ACTIVATED, { - action_type: 'started', - is_profile_syncing_enabled: isProfileSyncingEnabled, - }); + trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.NOTIFICATIONS_ACTIVATED, + ) + .addProperties({ + action_type: 'started', + is_profile_syncing_enabled: isProfileSyncingEnabled, + }) + .build(), + ); } } + const renderNetworkPicker = () => { + let networkPicker = ( + + ); + + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + if (isBtcAccount(selectedInternalAccount)) { + networkPicker = ( + + ); + } + ///: END:ONLY_INCLUDE_IF + + return {networkPicker}; + }; + return { headerTitle: () => ( { navigation.navigate(...createAccountSelectorNavDetails({})); }} - accountTypeLabel={getLabelTextByAddress(selectedAddress) || undefined} + accountTypeLabel={ + getLabelTextByAddress(formattedAddress) || undefined + } showAddress cellAccountContainerStyle={styles.account} testID={WalletViewSelectorsIDs.ACCOUNT_ICON} /> ), - headerLeft: () => ( - - - - ), + headerLeft: () => renderNetworkPicker(), headerRight: () => ( ), headerLeft: () => ( @@ -1676,10 +1735,23 @@ export function getSwapsQuotesNavbar(navigation, route, themeColors) { const selectedQuote = route.params?.selectedQuote; const quoteBegin = route.params?.quoteBegin; if (!selectedQuote) { - trackEvent(MetaMetricsEvents.QUOTES_REQUEST_CANCELLED, { - ...trade, - responseTime: new Date().getTime() - quoteBegin, - }); + trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.QUOTES_REQUEST_CANCELLED, + ) + .addProperties({ + token_from: trade.token_from, + token_to: trade.token_to, + request_type: trade.request_type, + custom_slippage: trade.custom_slippage, + chain_id: trade.chain_id, + responseTime: new Date().getTime() - quoteBegin, + }) + .addSensitiveProperties({ + token_from_amount: trade.token_from_amount, + }) + .build(), + ); } navigation.pop(); }; @@ -1689,10 +1761,23 @@ export function getSwapsQuotesNavbar(navigation, route, themeColors) { const selectedQuote = route.params?.selectedQuote; const quoteBegin = route.params?.quoteBegin; if (!selectedQuote) { - trackEvent(MetaMetricsEvents.QUOTES_REQUEST_CANCELLED, { - ...trade, - responseTime: new Date().getTime() - quoteBegin, - }); + trackEvent( + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.QUOTES_REQUEST_CANCELLED, + ) + .addProperties({ + token_from: trade.token_from, + token_to: trade.token_to, + request_type: trade.request_type, + custom_slippage: trade.custom_slippage, + chain_id: trade.chain_id, + responseTime: new Date().getTime() - quoteBegin, + }) + .addSensitiveProperties({ + token_from_amount: trade.token_from_amount, + }) + .build(), + ); } navigation.dangerouslyGetParent()?.pop(); }; diff --git a/app/components/UI/NavbarTitle/index.js b/app/components/UI/NavbarTitle/index.js index 87ba198e044..30b05eb77ae 100644 --- a/app/components/UI/NavbarTitle/index.js +++ b/app/components/UI/NavbarTitle/index.js @@ -6,7 +6,6 @@ import { TouchableOpacity, View, StyleSheet } from 'react-native'; import { fontStyles, colors as importedColors } from '../../../styles/common'; import Networks, { getDecimalChainId } from '../../../util/networks'; import { strings } from '../../../../locales/i18n'; -import Device from '../../../util/device'; import { ThemeContext, mockTheme } from '../../../util/theme'; import Routes from '../../../constants/navigation/Routes'; import { MetaMetricsEvents } from '../../../core/Analytics'; @@ -15,6 +14,7 @@ import { selectProviderConfig } from '../../../selectors/networkController'; import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import Text, { TextVariant, + TextColor, } from '../../../component-library/components/Texts/Text'; const createStyles = (colors) => @@ -22,37 +22,10 @@ const createStyles = (colors) => wrapper: { justifyContent: 'center', alignItems: 'center', - flex: 1, }, network: { flexDirection: 'row', - }, - networkName: { - fontSize: 11, - color: colors.text.alternative, - ...fontStyles.normal, - }, - networkIcon: { - width: 5, - height: 5, - borderRadius: 100, - marginRight: 5, - marginTop: Device.isIos() ? 4 : 5, - }, - title: { - fontSize: scale(14), - ...fontStyles.normal, - color: colors.text.default, - }, - children: { - ...fontStyles.normal, - color: colors.text.default, - fontWeight: 'bold', - }, - otherNetworkIcon: { - backgroundColor: importedColors.transparent, - borderColor: colors.border.default, - borderWidth: 1, + alignItems: 'center', }, }); @@ -90,6 +63,10 @@ class NavbarTitle extends PureComponent { * Boolean that specifies if the network selected is displayed */ showSelectedNetwork: PropTypes.bool, + /** + * Name of the network to display + */ + networkName: PropTypes.string, /** * Content to display inside text element */ @@ -112,10 +89,12 @@ class NavbarTitle extends PureComponent { }); this.props.metrics.trackEvent( - MetaMetricsEvents.NETWORK_SELECTOR_PRESSED, - { - chain_id: getDecimalChainId(this.props.providerConfig.chainId), - }, + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED) + .addProperties({ + chain_id: getDecimalChainId(this.props.providerConfig.chainId), + }) + .build(), ); setTimeout(() => { this.animating = false; @@ -125,8 +104,14 @@ class NavbarTitle extends PureComponent { }; render = () => { - const { providerConfig, title, translate, showSelectedNetwork, children } = - this.props; + const { + providerConfig, + title, + translate, + showSelectedNetwork, + children, + networkName, + } = this.props; let name = null; const color = (Networks[providerConfig.type] && Networks[providerConfig.type].color) || @@ -134,7 +119,9 @@ class NavbarTitle extends PureComponent { const colors = this.context.colors || mockTheme.colors; const styles = createStyles(colors); - if (providerConfig.nickname) { + if (networkName) { + name = networkName; + } else if (providerConfig.nickname) { name = providerConfig.nickname; } else { name = @@ -143,7 +130,6 @@ class NavbarTitle extends PureComponent { } const realTitle = translate ? strings(title) : title; - return ( {title ? ( - + {realTitle} ) : null} {typeof children === 'string' ? ( - - {strings(children)} - + {strings(children)} ) : ( children )} {showSelectedNetwork ? ( - - + {name} diff --git a/app/components/UI/NetworkAssetLogo/__snapshots__/index.test.tsx.snap b/app/components/UI/NetworkAssetLogo/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..315d31b1089 --- /dev/null +++ b/app/components/UI/NetworkAssetLogo/__snapshots__/index.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NetworkAssetLogo Component matches the snapshot for non-mainnet 1`] = `null`; diff --git a/app/components/UI/NetworkAssetLogo/index.test.tsx b/app/components/UI/NetworkAssetLogo/index.test.tsx new file mode 100644 index 00000000000..d283daa77d1 --- /dev/null +++ b/app/components/UI/NetworkAssetLogo/index.test.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import NetworkAssetLogo from '.'; +import TokenIcon from '../Swaps/components/TokenIcon'; +import { ChainId } from '@metamask/controller-utils'; + +// Mock the TokenIcon component +jest.mock('../Swaps/components/TokenIcon', () => jest.fn(() => null)); + +describe('NetworkAssetLogo Component', () => { + it('matches the snapshot for non-mainnet', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders TokenIcon with ETH for mainnet chainId', () => { + const props = { + chainId: ChainId.mainnet, + ticker: 'TEST', + style: { width: 50, height: 50 }, + big: true, + biggest: false, + testID: 'network-asset-logo', + }; + + render(); + + expect(TokenIcon).toHaveBeenCalledWith( + { + big: props.big, + biggest: props.biggest, + symbol: 'ETH', + style: props.style, + testID: props.testID, + }, + {}, + ); + }); + + it('renders TokenIcon with ticker for non-mainnet chainId', () => { + const props = { + chainId: '0x38', // Binance Smart Chain + ticker: 'BNB', + style: { width: 40, height: 40 }, + big: false, + biggest: true, + testID: 'network-asset-logo', + }; + + render(); + + expect(TokenIcon).toHaveBeenCalledWith( + { + big: props.big, + biggest: props.biggest, + symbol: props.ticker, + style: props.style, + testID: props.testID, + }, + {}, + ); + }); +}); diff --git a/app/components/UI/NetworkAssetLogo/index.tsx b/app/components/UI/NetworkAssetLogo/index.tsx new file mode 100644 index 00000000000..90629c5f871 --- /dev/null +++ b/app/components/UI/NetworkAssetLogo/index.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { ChainId } from '@metamask/controller-utils'; +import TokenIcon from '../Swaps/components/TokenIcon'; + +interface NetworkAssetLogoProps { + chainId: string; + ticker: string; + style: object; + big: boolean; + biggest: boolean; + testID: string; +} + +function NetworkAssetLogo({ + chainId, + ticker, + style, + big, + biggest, + testID, +}: NetworkAssetLogoProps) { + if (chainId === ChainId.mainnet) { + return ( + + ); + } + return ( + + ); +} + +export default NetworkAssetLogo; diff --git a/app/components/UI/NetworkCell/NetworkCell.tsx b/app/components/UI/NetworkCell/NetworkCell.tsx index 58ea3d26c5d..a9816e6c86a 100644 --- a/app/components/UI/NetworkCell/NetworkCell.tsx +++ b/app/components/UI/NetworkCell/NetworkCell.tsx @@ -1,23 +1,21 @@ import React from 'react'; import { Switch, ImageSourcePropType } from 'react-native'; -import { ETHERSCAN_SUPPORTED_NETWORKS } from '@metamask/transaction-controller'; import { useStyles } from '../../../component-library/hooks'; import Cell from '../../../component-library/components/Cells/Cell/Cell'; import { CellVariant } from '../../../component-library/components/Cells/Cell'; import { AvatarVariant } from '../../../component-library/components/Avatars/Avatar/Avatar.types'; import { useTheme } from '../../../util/theme'; -import { EtherscanSupportedHexChainId } from '@metamask/preferences-controller'; import styleSheet from './NetworkCell.styles'; +import { Hex } from '@metamask/utils'; -const supportedNetworks = ETHERSCAN_SUPPORTED_NETWORKS; interface NetworkCellProps { name: string; - chainId: EtherscanSupportedHexChainId | keyof typeof supportedNetworks; + chainId: Hex; imageSource: ImageSourcePropType; - secondaryText: string; + secondaryText?: string; showIncomingTransactionsNetworks: Record; toggleEnableIncomingTransactions: ( - chainId: EtherscanSupportedHexChainId, + chainId: Hex, value: boolean, ) => void; testID?: string; diff --git a/app/components/UI/NetworkModal/NetworkAdded/index.tsx b/app/components/UI/NetworkModal/NetworkAdded/index.tsx index 299781879c2..8a00786072f 100644 --- a/app/components/UI/NetworkModal/NetworkAdded/index.tsx +++ b/app/components/UI/NetworkModal/NetworkAdded/index.tsx @@ -1,15 +1,30 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; -import StyledButton from '../../StyledButton'; import { strings } from '../../../../../locales/i18n'; -import Text from '../../../Base/Text'; -import { useTheme } from '../../../../util/theme'; import { NetworkAddedBottomSheetSelectorsIDs } from '../../../../../e2e/selectors/Network/NetworkAddedBottomSheet.selectors'; +import BottomSheetHeader from '../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import Text, { + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { + ButtonProps, + ButtonVariants, + ButtonSize, +} from '../../../../component-library/components/Buttons/Button/Button.types'; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any -const createStyles = (colors: any) => +const createStyles = () => StyleSheet.create({ + header: { + padding: 0, + }, + content: { + paddingVertical: 16, + }, buttonView: { flexDirection: 'row', paddingVertical: 16, @@ -17,19 +32,6 @@ const createStyles = (colors: any) => base: { padding: 16, }, - button: { - flex: 1, - }, - cancel: { - marginRight: 8, - backgroundColor: colors.background.default, - borderColor: colors.border.default, - - borderWidth: 1, - }, - confirm: { - marginLeft: 8, - }, }); interface NetworkAddedProps { @@ -40,38 +42,45 @@ interface NetworkAddedProps { const NetworkAdded = (props: NetworkAddedProps) => { const { nickname, closeModal, switchNetwork } = props; - const { colors } = useTheme(); - const styles = createStyles(colors); + const styles = createStyles(); + const buttonProps: ButtonProps[] = [ + { + variant: ButtonVariants.Secondary, + size: ButtonSize.Lg, + onPress: closeModal, + label: strings('networks.close'), + testID: NetworkAddedBottomSheetSelectorsIDs.CLOSE_NETWORK_BUTTON, + }, + { + variant: ButtonVariants.Primary, + size: ButtonSize.Lg, + onPress: switchNetwork, + label: strings('networks.switch_network'), + testID: NetworkAddedBottomSheetSelectorsIDs.SWITCH_NETWORK_BUTTON, + }, + ]; return ( - + {strings('networks.new_network')} - - - {`"${strings('networks.network_name', { - networkName: nickname ?? '', - })}"`} - {strings('networks.network_added')} - - - - {strings('networks.close')} - - - {strings('networks.switch_network')} - + + + + + {`"${strings('networks.network_name', { + networkName: nickname ?? '', + })}"`} + + + {strings('networks.network_added')} + + + ); }; diff --git a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap index fa6b7d2bf5b..343d6450187 100644 --- a/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/NetworkModal/__snapshots__/index.test.tsx.snap @@ -146,7 +146,6 @@ exports[`NetworkDetails renders correctly 1`] = ` - - T - + testID="network-avatar-image" + /> ({ + context: { + PreferencesController: { + setTokenNetworkFilter: jest.fn(), + }, + NetworkController: { + updateNetwork: jest.fn(), + addNetwork: jest.fn(), + setActiveNetwork: jest.fn(), + }, + }, +})); + interface NetworkProps { isVisible: boolean; onClose: () => void; @@ -18,27 +36,46 @@ interface NetworkProps { showPopularNetworkModal: boolean; } +const mockDispatch = jest.fn(); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), - useDispatch: jest.fn(), + useDispatch: () => mockDispatch, useSelector: jest.fn(), })); + describe('NetworkDetails', () => { const props: NetworkProps = { isVisible: true, - onClose: () => ({}), + onClose: jest.fn(), networkConfiguration: { - chainId: '1', + chainId: '0x1', nickname: 'Test Network', - ticker: 'Test', + ticker: 'TEST', rpcUrl: 'https://localhost:8545', formattedRpcUrl: 'https://localhost:8545', rpcPrefs: { blockExplorerUrl: 'https://test.com', imageUrl: 'image' }, }, - navigation: 'navigation', + navigation: { navigate: jest.fn(), goBack: jest.fn() }, shouldNetworkSwitchPopToWallet: true, showPopularNetworkModal: true, }; + + beforeEach(() => { + jest.clearAllMocks(); + (useSelector as jest.Mock).mockImplementation((selector) => { + if (selector === selectNetworkName) return 'Ethereum Main Network'; + if (selector === selectUseSafeChainsListValidation) return true; + return {}; + }); + }); + + const renderWithTheme = (component: React.ReactNode) => + render( + + {component} + , + ); + it('renders correctly', () => { (useSelector as jest.MockedFn).mockImplementation( (selector) => { @@ -46,8 +83,31 @@ describe('NetworkDetails', () => { if (selector === selectUseSafeChainsListValidation) return true; }, ); - const { toJSON } = render(); + const { toJSON } = renderWithTheme(); expect(toJSON()).toMatchSnapshot(); }); + + it('should call setTokenNetworkFilter when switching networks', async () => { + const { getByTestId } = renderWithTheme(); + + const approveButton = getByTestId( + NetworkApprovalBottomSheetSelectorsIDs.APPROVE_BUTTON, + ); + fireEvent.press(approveButton); + + const switchButton = getByTestId( + NetworkAddedBottomSheetSelectorsIDs.SWITCH_NETWORK_BUTTON, + ); + await act(async () => { + fireEvent.press(switchButton); + }); + + expect( + Engine.context.PreferencesController.setTokenNetworkFilter, + ).toHaveBeenCalledWith({ + [props.networkConfiguration.chainId]: true, + }); + expect(mockDispatch).toHaveBeenCalled(); + }); }); diff --git a/app/components/UI/NetworkModal/index.tsx b/app/components/UI/NetworkModal/index.tsx index 3c58495c9f5..30d7d4e1efd 100644 --- a/app/components/UI/NetworkModal/index.tsx +++ b/app/components/UI/NetworkModal/index.tsx @@ -6,10 +6,8 @@ import Text from '../../Base/Text'; import NetworkDetails from './NetworkDetails'; import NetworkAdded from './NetworkAdded'; import Engine from '../../../core/Engine'; -import { - isPrivateConnection, - toggleUseSafeChainsListValidation, -} from '../../../util/networks'; +import { isPrivateConnection } from '../../../util/networks'; +import { toggleUseSafeChainsListValidation } from '../../../util/networks/engineNetworkUtils'; import getDecimalChainId from '../../../util/networks/getDecimalChainId'; import URLPARSE from 'url-parse'; import { isWebUri } from 'valid-url'; @@ -24,7 +22,10 @@ import { import { useTheme } from '../../../util/theme'; import { networkSwitched } from '../../../actions/onboardNetwork'; import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkApprovalBottomSheet.selectors'; -import { selectUseSafeChainsListValidation } from '../../../selectors/preferencesController'; +import { + selectTokenNetworkFilter, + selectUseSafeChainsListValidation, +} from '../../../selectors/preferencesController'; import BottomSheetFooter, { ButtonsAlignment, } from '../../../component-library/components/BottomSheets/BottomSheetFooter'; @@ -36,7 +37,10 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; import { toHex } from '@metamask/controller-utils'; import { rpcIdentifierUtility } from '../../../components/hooks/useSafeChains'; import Logger from '../../../util/Logger'; -import { selectNetworkConfigurations } from '../../../selectors/networkController'; +import { + selectNetworkConfigurations, + selectIsAllNetworks, +} from '../../../selectors/networkController'; import { NetworkConfiguration, RpcEndpointType, @@ -87,6 +91,7 @@ const NetworkModals = (props: NetworkProps) => { const [showDetails, setShowDetails] = React.useState(false); const [networkAdded, setNetworkAdded] = React.useState(false); const [showCheckNetwork, setShowCheckNetwork] = React.useState(false); + const tokenNetworkFilter = useSelector(selectTokenNetworkFilter); const [alerts, setAlerts] = React.useState< { alertError: string; @@ -98,6 +103,7 @@ const NetworkModals = (props: NetworkProps) => { const isCustomNetwork = true; const showDetailsModal = () => setShowDetails(!showDetails); const showCheckNetworkModal = () => setShowCheckNetwork(!showCheckNetwork); + const isAllNetworks = useSelector(selectIsAllNetworks); const { colors } = useTheme(); const styles = createNetworkModalStyles(colors); @@ -109,6 +115,30 @@ const NetworkModals = (props: NetworkProps) => { return true; }; + const customNetworkInformation = { + chainId, + blockExplorerUrl, + chainName: nickname, + rpcUrl, + icon: imageUrl, + ticker, + alerts, + }; + + const onUpdateNetworkFilter = useCallback(() => { + const { PreferencesController } = Engine.context; + if (!isAllNetworks) { + PreferencesController.setTokenNetworkFilter({ + [customNetworkInformation.chainId]: true, + }); + } else { + PreferencesController.setTokenNetworkFilter({ + ...tokenNetworkFilter, + [customNetworkInformation.chainId]: true, + }); + } + }, [customNetworkInformation.chainId, isAllNetworks, tokenNetworkFilter]); + const addNetwork = async () => { const isValidUrl = validateRpcUrl(rpcUrl); if (showPopularNetworkModal) { @@ -172,16 +202,6 @@ const NetworkModals = (props: NetworkProps) => { selectNetworkConfigurations, ); - const customNetworkInformation = { - chainId, - blockExplorerUrl, - chainName: nickname, - rpcUrl, - icon: imageUrl, - ticker, - alerts, - }; - const checkNetwork = useCallback(async () => { if (useSafeChainsListValidation) { const alertsNetwork = await checkSafeNetwork( @@ -245,6 +265,7 @@ const NetworkModals = (props: NetworkProps) => { } if (networkClientId) { + onUpdateNetworkFilter(); await NetworkController.setActiveNetwork(networkClientId); } @@ -270,7 +291,7 @@ const NetworkModals = (props: NetworkProps) => { const { networkClientId } = updatedNetwork?.rpcEndpoints?.[updatedNetwork.defaultRpcEndpointIndex] ?? {}; - + onUpdateNetworkFilter(); await NetworkController.setActiveNetwork(networkClientId); }; @@ -339,6 +360,7 @@ const NetworkModals = (props: NetworkProps) => { addedNetwork?.rpcEndpoints?.[addedNetwork.defaultRpcEndpointIndex] ?? {}; + onUpdateNetworkFilter(); NetworkController.setActiveNetwork(networkClientId); } onClose(); diff --git a/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx b/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx index 551146856e0..7fa0acdcb58 100644 --- a/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx +++ b/app/components/UI/NetworkSelectorList/NetworkSelectorList.tsx @@ -1,6 +1,6 @@ // Third party dependencies. import React, { useCallback, useRef } from 'react'; -import { ListRenderItem, ImageSourcePropType } from 'react-native'; +import { ListRenderItem, ImageSourcePropType, View } from 'react-native'; import { FlatList } from 'react-native-gesture-handler'; // External dependencies. @@ -53,23 +53,26 @@ const NetworkSelectorList = ({ if (selectedChainIds) { isSelectedNetwork = selectedChainIds.includes(id); } - return ( - onSelectNetwork?.(id, isSelectedNetwork)} - avatarProps={{ - variant: AvatarVariant.Network, - name, - imageSource: imageSource as ImageSourcePropType, - size: AvatarSize.Sm, - }} - disabled={isDisabled} + - {renderRightAccessory?.(id, name)} - + onSelectNetwork?.(id, isSelectedNetwork)} + avatarProps={{ + variant: AvatarVariant.Network, + name, + imageSource: imageSource as ImageSourcePropType, + size: AvatarSize.Sm, + }} + disabled={isDisabled} + > + {renderRightAccessory?.(id, name)} + + ); }, [ diff --git a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx index 96761b37689..7595f71cb85 100644 --- a/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx +++ b/app/components/UI/NetworkVerificationInfo/NetworkVerificationInfo.tsx @@ -37,9 +37,9 @@ import BottomSheetFooter, { import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader'; import { getNetworkImageSource, - toggleUseSafeChainsListValidation, isMultichainVersion1Enabled, } from '../../../util/networks'; +import { toggleUseSafeChainsListValidation } from '../../../util/networks/engineNetworkUtils'; import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkApprovalBottomSheet.selectors'; import hideKeyFromUrl from '../../../util/hideKeyFromUrl'; import { convertHexToDecimal } from '@metamask/controller-utils'; diff --git a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap index 7b3fccd6bdf..70c08524725 100644 --- a/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap +++ b/app/components/UI/NetworkVerificationInfo/__snapshots__/NetworkVerificationInfo.test.tsx.snap @@ -7,7 +7,6 @@ exports[`NetworkVerificationInfo renders correctly 1`] = ` StyleSheet.create({ @@ -154,7 +154,7 @@ const getTitle = (status, { nonce, amount, assetType }) => { }; export const getDescription = (status, { amount = null, type = null }) => { - if (amount && typeof amount !== 'object') { + if (amount && typeof amount !== 'object' && type) { return strings(`notifications.${type}_${status}_message`, { amount }); } return strings(`notifications.${status}_message`); diff --git a/app/components/UI/Notification/List/index.test.tsx b/app/components/UI/Notification/List/index.test.tsx index 5908fb62a6e..987450d0dc1 100644 --- a/app/components/UI/Notification/List/index.test.tsx +++ b/app/components/UI/Notification/List/index.test.tsx @@ -21,6 +21,11 @@ const mockNavigation = createNavigationProps({}); const mockTrackEvent = jest.fn(); +jest.mock('../../../../util/notifications/constants', () => ({ + ...jest.requireActual('../../../../util/notifications/constants'), + isNotificationsFeatureEnabled: () => true, +})); + jest.mock('../../../../util/notifications/services/NotificationService', () => ({ ...jest.requireActual('../../../../util/notifications/services/NotificationService'), getBadgeCount: jest.fn(), diff --git a/app/components/UI/Notification/List/index.tsx b/app/components/UI/Notification/List/index.tsx index afa08f1bc58..68b97d9d73c 100644 --- a/app/components/UI/Notification/List/index.tsx +++ b/app/components/UI/Notification/List/index.tsx @@ -7,7 +7,7 @@ import ScrollableTabView, { DefaultTabBarProps, TabBarProps, } from 'react-native-scrollable-tab-view'; -import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/NotificationsView.selectors'; +import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/NotificationsView.selectors'; import { strings } from '../../../../../locales/i18n'; import { hasNotificationComponents, @@ -59,7 +59,7 @@ function Loading() { export function NotificationsListItem(props: NotificationsListItemProps) { const { styles } = useStyles(); const { markNotificationAsRead } = useMarkNotificationAsRead(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const onNotificationClick = useCallback( (item: Notification) => { markNotificationAsRead([ @@ -83,23 +83,26 @@ export function NotificationsListItem(props: NotificationsListItemProps) { } }); - trackEvent(MetaMetricsEvents.NOTIFICATION_CLICKED, { - notification_id: item.id, - notification_type: item.type, - ...('chain_id' in item && { - chain_id: item.chain_id, - }), - previously_read: item.isRead, - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.NOTIFICATION_CLICKED) + .addProperties({ + notification_id: item.id, + notification_type: item.type, + previously_read: item.isRead, + ...('chain_id' in item && { chain_id: item.chain_id }), + }) + .build(), + ); }, - [markNotificationAsRead, props.navigation, trackEvent], + [markNotificationAsRead, props.navigation, trackEvent, createEventBuilder], ); const menuItemState = useMemo(() => { const notificationState = - props.notification?.type && hasNotificationComponents(props.notification.type) - ? NotificationComponentState[props.notification.type] - : undefined; + props.notification?.type && + hasNotificationComponents(props.notification.type) + ? NotificationComponentState[props.notification.type] + : undefined; return notificationState?.createMenuItem(props.notification); }, [props.notification]); @@ -172,7 +175,7 @@ function TabbedNotificationList(props: NotificationsListProps) { theme: { colors }, styles, } = useStyles(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const getListProps = useNotificationListProps(props); @@ -180,19 +183,25 @@ function TabbedNotificationList(props: NotificationsListProps) { (tabLabel: string) => { switch (tabLabel) { case strings('notifications.list.0'): - trackEvent(MetaMetricsEvents.ALL_NOTIFICATIONS); + trackEvent( + createEventBuilder(MetaMetricsEvents.ALL_NOTIFICATIONS).build(), + ); break; case strings('notifications.list.1'): - trackEvent(MetaMetricsEvents.WALLET_NOTIFICATIONS); + trackEvent( + createEventBuilder(MetaMetricsEvents.WALLET_NOTIFICATIONS).build(), + ); break; case strings('notifications.list.2'): - // trackEvent(MetaMetricsEvents.WEB3_NOTIFICATIONS); + // trackEvent( + // createEventBuilder(MetaMetricsEvents.WEB3_NOTIFICATIONS).build(), + // ); break; default: break; } }, - [trackEvent], + [trackEvent, createEventBuilder], ); return ( diff --git a/app/components/UI/Notification/ResetNotificationsModal/index.tsx b/app/components/UI/Notification/ResetNotificationsModal/index.tsx index ff45a3da4b9..49201392686 100644 --- a/app/components/UI/Notification/ResetNotificationsModal/index.tsx +++ b/app/components/UI/Notification/ResetNotificationsModal/index.tsx @@ -8,7 +8,7 @@ import BottomSheet, { } from '../../../../component-library/components/BottomSheets/BottomSheet'; import { strings } from '../../../../../locales/i18n'; -import { +import { IconColor, IconName, IconSize, @@ -19,10 +19,11 @@ import ModalContent from '../Modal'; import { ToastContext } from '../../../../component-library/components/Toast'; import { ToastVariants } from '../../../../component-library/components/Toast/Toast.types'; const ResetNotificationsModal = () => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const bottomSheetRef = useRef(null); const [isChecked, setIsChecked] = React.useState(false); - const { deleteNotificationsStorageKey, loading } = useDeleteNotificationsStorageKey(); + const { deleteNotificationsStorageKey, loading } = + useDeleteNotificationsStorageKey(); const { toastRef } = useContext(ToastContext); const closeBottomSheet = () => bottomSheetRef.current?.onCloseBottomSheet(); @@ -42,9 +43,11 @@ const ResetNotificationsModal = () => { const handleCta = async () => { await deleteNotificationsStorageKey().then(() => { showResultToast(); - trackEvent(MetaMetricsEvents.NOTIFICATION_STORAGE_KEY_DELETED, { - settings_type: 'delete_notifications_storage_key', - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.NOTIFICATION_STORAGE_KEY_DELETED) + .addProperties({ settings_type: 'delete_notifications_storage_key' }) + .build(), + ); }); }; @@ -56,7 +59,6 @@ const ResetNotificationsModal = () => { prevLoading.current = loading; }, [loading]); - return ( { handleCta={handleCta} handleCancel={closeBottomSheet} loading={loading} - /> + /> ); }; diff --git a/app/components/UI/Notification/TransactionNotification/index.js b/app/components/UI/Notification/TransactionNotification/index.js index e04e2dddacf..23c3c853855 100644 --- a/app/components/UI/Notification/TransactionNotification/index.js +++ b/app/components/UI/Notification/TransactionNotification/index.js @@ -35,7 +35,7 @@ import { selectTokensByAddress } from '../../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../../selectors/tokenRatesController'; import { selectAccounts } from '../../../../selectors/accountTrackerController'; import { speedUpTransaction } from '../../../../util/transaction-controller'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../../selectors/accountsController'; const WINDOW_WIDTH = Dimensions.get('window').width; const ACTION_CANCEL = 'cancel'; @@ -447,7 +447,7 @@ const mapStateToProps = (state) => { return { accounts: selectAccounts(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), transactions: TransactionController.transactions, ticker: selectTicker(state), chainId, diff --git a/app/components/UI/OnboardingWizard/Coachmark/index.js b/app/components/UI/OnboardingWizard/Coachmark/index.js index f84de8cf959..8be92c62e09 100644 --- a/app/components/UI/OnboardingWizard/Coachmark/index.js +++ b/app/components/UI/OnboardingWizard/Coachmark/index.js @@ -22,7 +22,7 @@ import { ButtonWidthTypes, } from '../../../../component-library/components/Buttons/Button'; import Button from '../../../../component-library/components/Buttons/Button/Button'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; const createStyles = (colors) => StyleSheet.create({ diff --git a/app/components/UI/OnboardingWizard/Step1/index.tsx b/app/components/UI/OnboardingWizard/Step1/index.tsx index 9ac7b417980..34c99d7c5b8 100644 --- a/app/components/UI/OnboardingWizard/Step1/index.tsx +++ b/app/components/UI/OnboardingWizard/Step1/index.tsx @@ -12,7 +12,7 @@ import { ONBOARDING_WIZARD_STEP_DESCRIPTION, } from '../../../../core/Analytics'; import { ThemeContext, mockTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../../components/hooks/useMetrics'; const styles = StyleSheet.create({ @@ -38,7 +38,7 @@ const Step1 = ({ onClose }: Step1Props) => { const theme = useContext(ThemeContext) || mockTheme; const dynamicOnboardingStyles = onboardingStyles(theme.colors); const dispatch = useDispatch(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const content = useCallback( () => ( @@ -54,11 +54,15 @@ const Step1 = ({ onClose }: Step1Props) => { const onNext = useCallback(() => { dispatch(setOnboardingWizardStep?.(2)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STARTED, { - tutorial_step_count: 1, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[1], - }); - }, [dispatch, trackEvent]); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STARTED) + .addProperties({ + tutorial_step_count: 1, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[1], + }) + .build(), + ); + }, [dispatch, trackEvent, createEventBuilder]); return ( { const { colors } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const dispatch = useDispatch(); const { coachmarkTop } = useHandleLayout(coachmarkRef); const onNext = () => { dispatch(setOnboardingWizardStep?.(3)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED, { - tutorial_step_count: 2, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[2], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED) + .addProperties({ + tutorial_step_count: 2, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[2], + }) + .build(), + ); }; const onBack = () => { dispatch(setOnboardingWizardStep?.(1)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED, { - tutorial_step_count: 2, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[2], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED) + .addProperties({ + tutorial_step_count: 2, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[2], + }) + .build(), + ); }; const getOnboardingStyles = () => onboardingStyles(colors); diff --git a/app/components/UI/OnboardingWizard/Step3/index.tsx b/app/components/UI/OnboardingWizard/Step3/index.tsx index 4e3c984b104..254884a0d92 100644 --- a/app/components/UI/OnboardingWizard/Step3/index.tsx +++ b/app/components/UI/OnboardingWizard/Step3/index.tsx @@ -12,7 +12,7 @@ import { } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; import { useMetrics } from '../../../hooks/useMetrics'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import useHandleLayout from '../useHandleLayout'; const styles = StyleSheet.create({ @@ -36,24 +36,32 @@ interface Step3Props { const Step3 = ({ coachmarkRef, onClose }: Step3Props) => { const { colors } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const dispatch = useDispatch(); const { coachmarkTop } = useHandleLayout(coachmarkRef); const onNext = () => { dispatch(setOnboardingWizardStep?.(4)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED, { - tutorial_step_count: 3, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[3], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED) + .addProperties({ + tutorial_step_count: 3, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[3], + }) + .build(), + ); }; const onBack = () => { dispatch(setOnboardingWizardStep?.(2)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED, { - tutorial_step_count: 3, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[3], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED) + .addProperties({ + tutorial_step_count: 3, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[3], + }) + .build(), + ); }; const getOnboardingStyles = () => onboardingStyles(colors); diff --git a/app/components/UI/OnboardingWizard/Step4/index.tsx b/app/components/UI/OnboardingWizard/Step4/index.tsx index 6e6538f4222..3b61ed84cc4 100644 --- a/app/components/UI/OnboardingWizard/Step4/index.tsx +++ b/app/components/UI/OnboardingWizard/Step4/index.tsx @@ -13,7 +13,7 @@ import { ONBOARDING_WIZARD_STEP_DESCRIPTION, } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../hooks/useMetrics'; @@ -35,7 +35,7 @@ interface Step4Props { const Step4 = ({ onClose }: Step4Props) => { const { colors } = useTheme(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const dispatch = useDispatch(); const [coachmarkTop, setCoachmarkTop] = useState(0); @@ -50,18 +50,26 @@ const Step4 = ({ onClose }: Step4Props) => { const onNext = () => { dispatch(setOnboardingWizardStep?.(5)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED, { - tutorial_step_count: 4, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[4], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED) + .addProperties({ + tutorial_step_count: 4, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[4], + }) + .build(), + ); }; const onBack = () => { dispatch(setOnboardingWizardStep?.(3)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED, { - tutorial_step_count: 4, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[4], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED) + .addProperties({ + tutorial_step_count: 4, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[4], + }) + .build(), + ); }; const getOnboardingStyles = () => onboardingStyles(colors); diff --git a/app/components/UI/OnboardingWizard/Step5/index.tsx b/app/components/UI/OnboardingWizard/Step5/index.tsx index 69fe4dcd685..df08abd3209 100644 --- a/app/components/UI/OnboardingWizard/Step5/index.tsx +++ b/app/components/UI/OnboardingWizard/Step5/index.tsx @@ -13,7 +13,7 @@ import { import { useTheme } from '../../../../util/theme'; import { useMetrics } from '../../../hooks/useMetrics'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; const styles = StyleSheet.create({ main: { @@ -36,7 +36,7 @@ interface Step5Props { } const Step5 = ({ onClose }: Step5Props) => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const { colors } = useTheme(); const dispatch = useDispatch(); const dynamicOnboardingStyles = onboardingStyles(colors); @@ -46,10 +46,14 @@ const Step5 = ({ onClose }: Step5Props) => { */ const onNext = () => { dispatch(setOnboardingWizardStep?.(6)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED, { - tutorial_step_count: 5, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[5], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED) + .addProperties({ + tutorial_step_count: 5, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[5], + }) + .build(), + ); }; /** @@ -57,10 +61,14 @@ const Step5 = ({ onClose }: Step5Props) => { */ const onBack = () => { dispatch(setOnboardingWizardStep?.(4)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED, { - tutorial_step_count: 5, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[5], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED) + .addProperties({ + tutorial_step_count: 5, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[5], + }) + .build(), + ); }; /** diff --git a/app/components/UI/OnboardingWizard/Step6/index.tsx b/app/components/UI/OnboardingWizard/Step6/index.tsx index 79bda1f8770..944dca5bc90 100644 --- a/app/components/UI/OnboardingWizard/Step6/index.tsx +++ b/app/components/UI/OnboardingWizard/Step6/index.tsx @@ -13,7 +13,7 @@ import { ONBOARDING_WIZARD_STEP_DESCRIPTION, } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../hooks/useMetrics'; const styles = StyleSheet.create({ @@ -38,7 +38,7 @@ interface Step6Props { } const Step6 = ({ onClose, navigation }: Step6Props) => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const { colors } = useTheme(); const dynamicOnboardingStyles = onboardingStyles(colors); const dispatch = useDispatch(); @@ -48,10 +48,14 @@ const Step6 = ({ onClose, navigation }: Step6Props) => { const onNext = () => { dispatch(setOnboardingWizardStep?.(7)); navigation?.navigate(...createBrowserNavDetails()); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED, { - tutorial_step_count: 6, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[6], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_COMPLETED) + .addProperties({ + tutorial_step_count: 6, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[6], + }) + .build(), + ); }; /** @@ -59,10 +63,14 @@ const Step6 = ({ onClose, navigation }: Step6Props) => { */ const onBack = () => { dispatch(setOnboardingWizardStep?.(5)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED, { - tutorial_step_count: 6, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[6], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED) + .addProperties({ + tutorial_step_count: 6, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[6], + }) + .build(), + ); }; /** diff --git a/app/components/UI/OnboardingWizard/Step7/index.tsx b/app/components/UI/OnboardingWizard/Step7/index.tsx index 661a65ee593..c71c6414ea8 100644 --- a/app/components/UI/OnboardingWizard/Step7/index.tsx +++ b/app/components/UI/OnboardingWizard/Step7/index.tsx @@ -14,7 +14,7 @@ import { } from '../../../../core/Analytics'; import { useTheme } from '../../../../util/theme'; -import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/OnboardingWizardModal.selectors'; +import { OnboardingWizardModalSelectorsIDs } from '../../../../../e2e/selectors/Onboarding/OnboardingWizardModal.selectors'; import { useMetrics } from '../../../hooks/useMetrics'; const styles = StyleSheet.create({ @@ -37,7 +37,7 @@ interface Step7Props { } const Step7 = ({ navigation, onClose }: Step7Props) => { - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const dispatch = useDispatch(); const [ready, setReady] = useState(false); const [coachmarkTop, setCoachmarkTop] = useState(0); @@ -65,10 +65,14 @@ const Step7 = ({ navigation, onClose }: Step7Props) => { setTimeout(() => { dispatch(setOnboardingWizardStep?.(6)); }, 1); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED, { - tutorial_step_count: 7, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[7], - }); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_STEP_REVISITED) + .addProperties({ + tutorial_step_count: 7, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[7], + }) + .build(), + ); }; /** diff --git a/app/components/UI/OnboardingWizard/index.tsx b/app/components/UI/OnboardingWizard/index.tsx index 189db8a7699..3f96607358e 100644 --- a/app/components/UI/OnboardingWizard/index.tsx +++ b/app/components/UI/OnboardingWizard/index.tsx @@ -91,7 +91,7 @@ const OnboardingWizard = ({ const { drawerRef } = useContext(DrawerContext); const theme = useTheme(); const dispatch = useDispatch(); - const { trackEvent } = useMetrics(); + const { trackEvent, createEventBuilder } = useMetrics(); const styles = createStyles(theme); const isAutomaticSecurityChecksModalOpen = useSelector( @@ -106,11 +106,17 @@ const OnboardingWizard = ({ const closeOnboardingWizard = async () => { await StorageWrapper.setItem(ONBOARDING_WIZARD, EXPLORED); dispatch(setOnboardingWizardStep(0)); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_SKIPPED, { - tutorial_step_count: step, - tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[step], - }); - trackEvent(MetaMetricsEvents.ONBOARDING_TOUR_COMPLETED); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_SKIPPED) + .addProperties({ + tutorial_step_count: step, + tutorial_step_name: ONBOARDING_WIZARD_STEP_DESCRIPTION[step], + }) + .build(), + ); + trackEvent( + createEventBuilder(MetaMetricsEvents.ONBOARDING_TOUR_COMPLETED).build(), + ); }; // Since react-native-default-preference is not covered by the fixtures, diff --git a/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap b/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap index cd0ccc8f400..f9950e7ce2b 100644 --- a/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/OptinMetrics/__snapshots__/index.test.tsx.snap @@ -694,7 +694,7 @@ exports[`OptinMetrics render matches snapshot 1`] = ` } } > - We'll use this data to learn how you interact with our marketing communications. We may share relavent news (like product features). + We'll use this data to learn how you interact with our marketing communications. We may share relevant news (like product features). diff --git a/app/components/UI/OptinMetrics/index.js b/app/components/UI/OptinMetrics/index.js index d765fd8f60d..58506d586b3 100644 --- a/app/components/UI/OptinMetrics/index.js +++ b/app/components/UI/OptinMetrics/index.js @@ -385,12 +385,17 @@ class OptinMetrics extends PureComponent { this.props.clearOnboardingEvents(); // track event for user opting in on metrics and data collection for marketing - metrics.trackEvent(MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED, { - ...dataCollectionForMarketingTraits, - is_metrics_opted_in: true, - location: 'onboarding_metametrics', - updated_after_onboarding: false, - }); + metrics.trackEvent( + metrics + .createEventBuilder(MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED) + .addProperties({ + ...dataCollectionForMarketingTraits, + is_metrics_opted_in: true, + location: 'onboarding_metametrics', + updated_after_onboarding: false, + }) + .build(), + ); }); this.continue(); }; diff --git a/app/components/UI/OptinMetrics/index.test.tsx b/app/components/UI/OptinMetrics/index.test.tsx index f101dad8312..3072907108e 100644 --- a/app/components/UI/OptinMetrics/index.test.tsx +++ b/app/components/UI/OptinMetrics/index.test.tsx @@ -3,6 +3,7 @@ import { renderScreen } from '../../../util/test/renderWithProvider'; import { MetaMetrics, MetaMetricsEvents } from '../../../core/Analytics'; import { fireEvent, screen, waitFor } from '@testing-library/react-native'; import { strings } from '../../../../locales/i18n'; +import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder'; const { InteractionManager } = jest.requireActual('react-native'); @@ -60,12 +61,15 @@ describe('OptinMetrics', () => { await waitFor(() => { expect(mockMetrics.trackEvent).toHaveBeenNthCalledWith( 1, - MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED, - { - is_metrics_opted_in: true, - location: 'onboarding_metametrics', - updated_after_onboarding: false, - }, + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED, + ) + .addProperties({ + is_metrics_opted_in: true, + location: 'onboarding_metametrics', + updated_after_onboarding: false, + }) + .build(), ); expect(mockMetrics.addTraitsToUser).toHaveBeenNthCalledWith(1, { deviceProp: 'Device value', @@ -86,13 +90,16 @@ describe('OptinMetrics', () => { await waitFor(() => { expect(mockMetrics.trackEvent).toHaveBeenNthCalledWith( 1, - MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED, - { - has_marketing_consent: true, - is_metrics_opted_in: true, - location: 'onboarding_metametrics', - updated_after_onboarding: false, - }, + MetricsEventBuilder.createEventBuilder( + MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED, + ) + .addProperties({ + has_marketing_consent: true, + is_metrics_opted_in: true, + location: 'onboarding_metametrics', + updated_after_onboarding: false, + }) + .build(), ); expect(mockMetrics.addTraitsToUser).toHaveBeenNthCalledWith(1, { deviceProp: 'Device value', diff --git a/app/components/UI/PaymentRequest/AssetList/index.tsx b/app/components/UI/PaymentRequest/AssetList/index.tsx index d3e5975e2a3..499817c6065 100644 --- a/app/components/UI/PaymentRequest/AssetList/index.tsx +++ b/app/components/UI/PaymentRequest/AssetList/index.tsx @@ -9,6 +9,7 @@ import { useSelector } from 'react-redux'; import { toChecksumAddress } from 'ethereumjs-util'; import { useTheme } from '../../../../util/theme'; import { selectTokenList } from '../../../../selectors/tokenListController'; +import { ImportTokenViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/ImportTokenView.selectors'; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -111,7 +112,7 @@ const AssetList = ({ ); return ( - + { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/app/components/UI/PaymentRequest/index.js b/app/components/UI/PaymentRequest/index.js index bdbb3bb8f90..5391e0b8816 100644 --- a/app/components/UI/PaymentRequest/index.js +++ b/app/components/UI/PaymentRequest/index.js @@ -58,7 +58,7 @@ import { import { selectTokenListArray } from '../../../selectors/tokenListController'; import { selectTokens } from '../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; -import { selectSelectedInternalAccountChecksummedAddress } from '../../../selectors/accountsController'; +import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; import { RequestPaymentViewSelectors } from '../../../../e2e/selectors/Receive/RequestPaymentView.selectors'; @@ -896,7 +896,7 @@ const mapStateToProps = (state) => ({ contractExchangeRates: selectContractExchangeRates(state), searchEngine: state.settings.searchEngine, tokens: selectTokens(state), - selectedAddress: selectSelectedInternalAccountChecksummedAddress(state), + selectedAddress: selectSelectedInternalAccountFormattedAddress(state), primaryCurrency: state.settings.primaryCurrency, ticker: selectTicker(state), chainId: selectChainId(state), diff --git a/app/components/UI/PaymentRequest/index.test.tsx b/app/components/UI/PaymentRequest/index.test.tsx index 391fe7e3b89..a01c0814d2f 100644 --- a/app/components/UI/PaymentRequest/index.test.tsx +++ b/app/components/UI/PaymentRequest/index.test.tsx @@ -39,6 +39,11 @@ const initialState = { }, }, tokens: [], + allTokens: { + '0x1': { + '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272': [], + }, + }, }, NetworkController: { provider: { @@ -50,7 +55,7 @@ const initialState = { ...MOCK_ACCOUNTS_CONTROLLER_STATE, internalAccounts: { ...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts, - selectedAccount: {}, + selectedAccount: '30786334-3935-4563-b064-363339643939', }, }, TokenListController: { diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx index 1d31aec69d9..cf998ed53a2 100644 --- a/app/components/UI/PermissionsSummary/PermissionsSummary.tsx +++ b/app/components/UI/PermissionsSummary/PermissionsSummary.tsx @@ -45,6 +45,9 @@ import { SDKSelectorsIDs } from '../../../../e2e/selectors/Settings/SDK.selector import { useSelector } from 'react-redux'; import { selectProviderConfig } from '../../../selectors/networkController'; import { useNetworkInfo } from '../../../selectors/selectedNetworkController'; +import { ConnectedAccountsSelectorsIDs } from '../../../../e2e/selectors/Browser/ConnectedAccountModal.selectors'; +import { PermissionSummaryBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Browser/PermissionSummaryBottomSheet.selectors'; +import { NetworkNonPemittedBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkNonPemittedBottomSheet.selectors'; const PermissionsSummary = ({ currentPageInformation, @@ -176,13 +179,9 @@ const PermissionsSummary = ({ } const renderEndAccessory = () => ( - + {isAlreadyConnected ? ( - + ) : ( - + + {renderHeader()} - + {isNonDappNetworkSwitch ? strings('permissions.title_add_network_permission') @@ -434,6 +446,9 @@ const PermissionsSummary = ({