From 771a2edb2eaee80313d2dbbb0258b36ee5e2e3c4 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:15:23 -0400 Subject: [PATCH 01/12] test: [POM] Migrate Snap Simple Keyring page and Snap List page to page object modal (#27327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This pull request migrates the remove snap account test to the Page Object Model (POM) pattern, enhancing code maintainability and improving test reliability. It also: - Created Snap Simple Keyring page class - Created Snap List page class - Avoided delays in the initial implementation for functions on the Snap Simple Keyring page ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: #27484 #27652 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Chloe Gao Co-authored-by: chloeYue <105063779+chloeYue@users.noreply.github.com> Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- test/e2e/accounts/remove-account-snap.spec.ts | 94 ---------- .../flows/snap-simple-keyring.flow.ts | 23 +++ .../page-objects/pages/account-list-page.ts | 93 +++++----- test/e2e/page-objects/pages/header-navbar.ts | 9 + test/e2e/page-objects/pages/settings-page.ts | 2 +- test/e2e/page-objects/pages/snap-list-page.ts | 80 +++++++++ .../pages/snap-simple-keyring-page.ts | 167 ++++++++++++++++++ .../tests/account/remove-account-snap.spec.ts | 50 ++++++ 8 files changed, 377 insertions(+), 141 deletions(-) delete mode 100644 test/e2e/accounts/remove-account-snap.spec.ts create mode 100644 test/e2e/page-objects/flows/snap-simple-keyring.flow.ts create mode 100644 test/e2e/page-objects/pages/snap-list-page.ts create mode 100644 test/e2e/page-objects/pages/snap-simple-keyring-page.ts create mode 100644 test/e2e/tests/account/remove-account-snap.spec.ts diff --git a/test/e2e/accounts/remove-account-snap.spec.ts b/test/e2e/accounts/remove-account-snap.spec.ts deleted file mode 100644 index f4b8e025c62d..000000000000 --- a/test/e2e/accounts/remove-account-snap.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { strict as assert } from 'assert'; -import { Suite } from 'mocha'; -import FixtureBuilder from '../fixture-builder'; -import { WINDOW_TITLES, defaultGanacheOptions, withFixtures } from '../helpers'; -import { Driver } from '../webdriver/driver'; -import { installSnapSimpleKeyring, makeNewAccountAndSwitch } from './common'; - -describe('Remove Account Snap', function (this: Suite) { - it('disable a snap and remove it', async function () { - await withFixtures( - { - fixtures: new FixtureBuilder().build(), - ganacheOptions: defaultGanacheOptions, - title: this.test?.fullTitle(), - }, - async ({ driver }: { driver: Driver }) => { - await installSnapSimpleKeyring(driver, false); - - await makeNewAccountAndSwitch(driver); - - // Check accounts after adding the snap account. - await driver.clickElement('[data-testid="account-menu-icon"]'); - const accountMenuItemsWithSnapAdded = await driver.findElements( - '.multichain-account-list-item', - ); - await driver.clickElement('.mm-box button[aria-label="Close"]'); - - // Navigate to settings. - await driver.switchToWindowWithTitle( - WINDOW_TITLES.ExtensionInFullScreenView, - ); - - await driver.clickElement( - '[data-testid="account-options-menu-button"]', - ); - - await driver.clickElement({ text: 'Snaps', tag: 'div' }); - await driver.clickElement({ - text: 'MetaMask Simple Snap Keyring', - tag: 'p', - }); - - // Disable the snap. - await driver.clickElement('.toggle-button > div'); - - // Remove the snap. - const removeButton = await driver.findElement( - '[data-testid="remove-snap-button"]', - ); - await driver.scrollToElement(removeButton); - await driver.clickElement('[data-testid="remove-snap-button"]'); - - await driver.clickElement({ - text: 'Continue', - tag: 'button', - }); - - await driver.fill( - '[data-testid="remove-snap-confirmation-input"]', - 'MetaMask Simple Snap Keyring', - ); - - await driver.clickElement({ - text: 'Remove Snap', - tag: 'button', - }); - - // Checking result modal - await driver.findVisibleElement({ - text: 'MetaMask Simple Snap Keyring removed', - tag: 'p', - }); - - // Assert that the snap was removed. - await driver.findElement({ - css: '.mm-box', - text: "You don't have any snaps installed.", - tag: 'p', - }); - await driver.clickElement('.mm-box button[aria-label="Close"]'); - - // Assert that an account was removed. - await driver.clickElement('[data-testid="account-menu-icon"]'); - const accountMenuItemsAfterRemoval = await driver.findElements( - '.multichain-account-list-item', - ); - assert.equal( - accountMenuItemsAfterRemoval.length, - accountMenuItemsWithSnapAdded.length - 1, - ); - }, - ); - }); -}); diff --git a/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts new file mode 100644 index 000000000000..68febce34b6b --- /dev/null +++ b/test/e2e/page-objects/flows/snap-simple-keyring.flow.ts @@ -0,0 +1,23 @@ +import { Driver } from '../../webdriver/driver'; +import SnapSimpleKeyringPage from '../pages/snap-simple-keyring-page'; +import { TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL } from '../../constants'; + +/** + * Go to the Snap Simple Keyring page and install the snap. + * + * @param driver - The WebDriver instance used to interact with the browser. + * @param isSyncFlow - Indicates whether to toggle on the use synchronous approval option on the snap. Defaults to true. + */ +export async function installSnapSimpleKeyring( + driver: Driver, + isSyncFlow: boolean = true, +) { + await driver.openNewPage(TEST_SNAPS_SIMPLE_KEYRING_WEBSITE_URL); + + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + await snapSimpleKeyringPage.check_pageIsLoaded(); + await snapSimpleKeyringPage.installSnap(); + if (isSyncFlow) { + await snapSimpleKeyringPage.toggleUseSyncApproval(); + } +} diff --git a/test/e2e/page-objects/pages/account-list-page.ts b/test/e2e/page-objects/pages/account-list-page.ts index 05c394444c36..03bdeef1579d 100644 --- a/test/e2e/page-objects/pages/account-list-page.ts +++ b/test/e2e/page-objects/pages/account-list-page.ts @@ -1,69 +1,58 @@ import { Driver } from '../../webdriver/driver'; class AccountListPage { - private driver: Driver; + private readonly driver: Driver; - private accountListItem: string; + private readonly accountListItem = + '.multichain-account-menu-popover__list--menu-item'; - private accountMenuButton: string; + private readonly accountMenuButton = + '[data-testid="account-list-menu-details"]'; - private accountNameInput: string; + private readonly accountNameInput = '#account-name'; - private accountOptionsMenuButton: string; + private readonly accountOptionsMenuButton = + '[data-testid="account-list-item-menu-button"]'; - private addAccountConfirmButton: string; + private readonly addAccountConfirmButton = + '[data-testid="submit-add-account-with-name"]'; - private addEthereumAccountButton: string; + private readonly addEthereumAccountButton = + '[data-testid="multichain-account-menu-popover-add-account"]'; - private addSnapAccountButton: object; + private readonly addSnapAccountButton = { + text: 'Add account Snap', + tag: 'button', + }; - private closeAccountModalButton: string; + private readonly closeAccountModalButton = 'button[aria-label="Close"]'; - private createAccountButton: string; + private readonly createAccountButton = + '[data-testid="multichain-account-menu-popover-action-button"]'; - private editableLabelButton: string; + private readonly editableLabelButton = + '[data-testid="editable-label-button"]'; - private editableLabelInput: string; + private readonly editableLabelInput = '[data-testid="editable-input"] input'; - private hideUnhideAccountButton: string; + private readonly hideUnhideAccountButton = + '[data-testid="account-list-menu-hide"]'; - private hiddenAccountOptionsMenuButton: string; + private readonly hiddenAccountOptionsMenuButton = + '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; - private hiddenAccountsList: string; + private readonly hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; - private pinUnpinAccountButton: string; + private readonly pinUnpinAccountButton = + '[data-testid="account-list-menu-pin"]'; - private pinnedIcon: string; + private readonly pinnedIcon = '[data-testid="account-pinned-icon"]'; - private saveAccountLabelButton: string; + private readonly saveAccountLabelButton = + '[data-testid="save-account-label-input"]'; constructor(driver: Driver) { this.driver = driver; - this.accountListItem = '.multichain-account-menu-popover__list--menu-item'; - this.accountMenuButton = '[data-testid="account-list-menu-details"]'; - this.accountNameInput = '#account-name'; - this.accountOptionsMenuButton = - '[data-testid="account-list-item-menu-button"]'; - this.addAccountConfirmButton = - '[data-testid="submit-add-account-with-name"]'; - this.addEthereumAccountButton = - '[data-testid="multichain-account-menu-popover-add-account"]'; - this.addSnapAccountButton = { - text: 'Add account Snap', - tag: 'button', - }; - this.closeAccountModalButton = 'button[aria-label="Close"]'; - this.createAccountButton = - '[data-testid="multichain-account-menu-popover-action-button"]'; - this.editableLabelButton = '[data-testid="editable-label-button"]'; - this.editableLabelInput = '[data-testid="editable-input"] input'; - this.hideUnhideAccountButton = '[data-testid="account-list-menu-hide"]'; - this.hiddenAccountOptionsMenuButton = - '.multichain-account-menu-popover__list--menu-item-hidden-account [data-testid="account-list-item-menu-button"]'; - this.hiddenAccountsList = '[data-testid="hidden-accounts-list"]'; - this.pinUnpinAccountButton = '[data-testid="account-list-menu-pin"]'; - this.pinnedIcon = '[data-testid="account-pinned-icon"]'; - this.saveAccountLabelButton = '[data-testid="save-account-label-input"]'; } async check_pageIsLoaded(): Promise { @@ -179,9 +168,21 @@ class AccountListPage { }); } - async check_accountIsDisplayed(): Promise { - console.log(`Check that account is displayed in account list`); - await this.driver.waitForSelector(this.accountListItem); + /** + * Checks that the account with the specified label is not displayed in the account list. + * + * @param expectedLabel - The label of the account that should not be displayed. + */ + async check_accountIsNotDisplayedInAccountList( + expectedLabel: string, + ): Promise { + console.log( + `Check that account label ${expectedLabel} is not displayed in account list`, + ); + await this.driver.assertElementNotPresent({ + css: this.accountListItem, + text: expectedLabel, + }); } async check_accountIsPinned(): Promise { diff --git a/test/e2e/page-objects/pages/header-navbar.ts b/test/e2e/page-objects/pages/header-navbar.ts index eb47cee791d9..495f7ddbf3c8 100644 --- a/test/e2e/page-objects/pages/header-navbar.ts +++ b/test/e2e/page-objects/pages/header-navbar.ts @@ -13,6 +13,8 @@ class HeaderNavbar { private settingsButton: string; + private accountSnapButton: object; + constructor(driver: Driver) { this.driver = driver; this.accountMenuButton = '[data-testid="account-menu-icon"]'; @@ -20,6 +22,7 @@ class HeaderNavbar { this.lockMetaMaskButton = '[data-testid="global-menu-lock"]'; this.mmiPortfolioButton = '[data-testid="global-menu-mmi-portfolio"]'; this.settingsButton = '[data-testid="global-menu-settings"]'; + this.accountSnapButton = { text: 'Snaps', tag: 'div' }; } async lockMetaMask(): Promise { @@ -35,6 +38,12 @@ class HeaderNavbar { await this.driver.clickElement(this.accountMenuButton); } + async openSnapListPage(): Promise { + console.log('Open account snap page'); + await this.driver.clickElement(this.accountOptionMenu); + await this.driver.clickElement(this.accountSnapButton); + } + async openSettingsPage(): Promise { console.log('Open settings page'); await this.driver.clickElement(this.accountOptionMenu); diff --git a/test/e2e/page-objects/pages/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts index 89678c8712ac..547f9e43a34e 100644 --- a/test/e2e/page-objects/pages/settings-page.ts +++ b/test/e2e/page-objects/pages/settings-page.ts @@ -29,7 +29,7 @@ class SettingsPage { } async goToExperimentalSettings(): Promise { - console.log('Navigating to Experimental Settings'); + console.log('Navigating to Experimental Settings page'); await this.driver.clickElement(this.experimentalSettingsButton); } } diff --git a/test/e2e/page-objects/pages/snap-list-page.ts b/test/e2e/page-objects/pages/snap-list-page.ts new file mode 100644 index 000000000000..b293340ea637 --- /dev/null +++ b/test/e2e/page-objects/pages/snap-list-page.ts @@ -0,0 +1,80 @@ +import { Driver } from '../../webdriver/driver'; + +class SnapListPage { + private readonly driver: Driver; + + private readonly closeModalButton = 'button[aria-label="Close"]'; + + private readonly continueRemoveSnapButton = { + tag: 'button', + text: 'Continue', + }; + + private readonly continueRemoveSnapModalMessage = { + tag: 'p', + text: 'Removing this Snap removes these accounts from MetaMask', + }; + + private readonly noSnapInstalledMessage = { + tag: 'p', + text: "You don't have any snaps installed.", + }; + + private readonly removeSnapButton = '[data-testid="remove-snap-button"]'; + + private readonly removeSnapConfirmationInput = + '[data-testid="remove-snap-confirmation-input"]'; + + private readonly removeSnapConfirmButton = { + tag: 'button', + text: 'Remove Snap', + }; + + // this selector needs to be combined with snap name to be unique. + private readonly snapListItem = '.snap-list-item'; + + constructor(driver: Driver) { + this.driver = driver; + } + + /** + * Removes a snap by its name from the snap list. + * + * @param snapName - The name of the snap to be removed. + */ + async removeSnapByName(snapName: string): Promise { + console.log('Removing snap on snap list page with name: ', snapName); + await this.driver.clickElement({ text: snapName, css: this.snapListItem }); + + const removeButton = await this.driver.findElement(this.removeSnapButton); + // The need to scroll to the element before clicking it is due to a bug in the Snap test dapp page. + // This bug has been fixed in the Snap test dapp page (PR here: https://github.com/MetaMask/snaps/pull/2782), which should mitigate the flaky issue of scrolling and clicking elements in the Snap test dapp. + // TODO: Once the Snaps team releases the new version with the fix, we'll be able to remove these scrolling steps and just use clickElement (which already handles scrolling). + await this.driver.scrollToElement(removeButton); + await this.driver.clickElement(this.removeSnapButton); + + await this.driver.waitForSelector(this.continueRemoveSnapModalMessage); + await this.driver.clickElement(this.continueRemoveSnapButton); + + console.log(`Fill confirmation input to confirm snap removal`); + await this.driver.waitForSelector(this.removeSnapConfirmationInput); + await this.driver.fill(this.removeSnapConfirmationInput, snapName); + await this.driver.clickElementAndWaitToDisappear( + this.removeSnapConfirmButton, + ); + + console.log(`Check snap removal success message is displayed`); + await this.driver.waitForSelector({ + text: `${snapName} removed`, + tag: 'p', + }); + await this.driver.clickElementAndWaitToDisappear(this.closeModalButton); + } + + async check_noSnapInstalledMessageIsDisplayed(): Promise { + console.log('Verifying no snaps is installed for current account'); + await this.driver.waitForSelector(this.noSnapInstalledMessage); + } +} + +export default SnapListPage; diff --git a/test/e2e/page-objects/pages/snap-simple-keyring-page.ts b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts new file mode 100644 index 000000000000..8b20f1bc3bc0 --- /dev/null +++ b/test/e2e/page-objects/pages/snap-simple-keyring-page.ts @@ -0,0 +1,167 @@ +import { Driver } from '../../webdriver/driver'; +import { WINDOW_TITLES } from '../../helpers'; + +class SnapSimpleKeyringPage { + private readonly driver: Driver; + + private readonly accountCreatedMessage = { + text: 'Account created', + tag: 'h3', + }; + + private readonly accountSupportedMethods = { + text: 'Account Supported Methods', + tag: 'p', + }; + + private readonly addtoMetamaskMessage = { + text: 'Add to MetaMask', + tag: 'h3', + }; + + private readonly confirmAddtoMetamask = { + text: 'Confirm', + tag: 'button', + }; + + private readonly confirmationSubmitButton = + '[data-testid="confirmation-submit-button"]'; + + private readonly confirmCompleteButton = { + text: 'OK', + tag: 'button', + }; + + private readonly confirmConnectionButton = { + text: 'Connect', + tag: 'button', + }; + + private readonly connectButton = '#connectButton'; + + private readonly createAccountButton = { + text: 'Create Account', + tag: 'button', + }; + + private readonly createAccountMessage = + '[data-testid="create-snap-account-content-title"]'; + + private readonly createAccountSection = { + text: 'Create account', + tag: 'div', + }; + + private readonly createSnapAccountName = '#account-name'; + + private readonly installationCompleteMessage = { + text: 'Installation complete', + tag: 'h2', + }; + + private readonly pageTitle = { + text: 'Snap Simple Keyring', + tag: 'p', + }; + + private readonly snapConnectedMessage = '#snapConnected'; + + private readonly snapInstallScrollButton = + '[data-testid="snap-install-scroll"]'; + + private readonly submitAddAccountWithNameButton = + '[data-testid="submit-add-account-with-name"]'; + + private readonly useSyncApprovalToggle = + '[data-testid="use-sync-flow-toggle"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.pageTitle, + this.useSyncApprovalToggle, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for Snap Simple Keyring page to be loaded', + e, + ); + throw e; + } + console.log('Snap Simple Keyring page is loaded'); + } + + /** + * Creates a new account on the Snap Simple Keyring page and checks the account is created. + */ + async createNewAccount(): Promise { + console.log('Create new account on Snap Simple Keyring page'); + await this.driver.clickElement(this.createAccountSection); + await this.driver.clickElement(this.createAccountButton); + + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.waitForSelector(this.createAccountMessage); + await this.driver.clickElement(this.confirmationSubmitButton); + + await this.driver.waitForSelector(this.createSnapAccountName); + await this.driver.clickElement(this.submitAddAccountWithNameButton); + + await this.driver.waitForSelector(this.accountCreatedMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmationSubmitButton, + ); + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await this.check_accountSupportedMethodsDisplayed(); + } + + /** + * Installs the Simple Keyring Snap and checks the snap is connected. + */ + async installSnap(): Promise { + console.log('Install Simple Keyring Snap'); + await this.driver.clickElement(this.connectButton); + + await this.driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await this.driver.clickElement(this.confirmConnectionButton); + + await this.driver.waitForSelector(this.addtoMetamaskMessage); + await this.driver.clickElementSafe(this.snapInstallScrollButton, 200); + await this.driver.waitForSelector(this.confirmAddtoMetamask); + await this.driver.clickElement(this.confirmAddtoMetamask); + + await this.driver.waitForSelector(this.installationCompleteMessage); + await this.driver.clickElementAndWaitForWindowToClose( + this.confirmCompleteButton, + ); + + await this.driver.switchToWindowWithTitle( + WINDOW_TITLES.SnapSimpleKeyringDapp, + ); + await this.check_simpleKeyringSnapConnected(); + } + + async toggleUseSyncApproval() { + console.log('Toggle Use Synchronous Approval'); + await this.driver.clickElement(this.useSyncApprovalToggle); + } + + async check_accountSupportedMethodsDisplayed(): Promise { + console.log( + 'Check new created account supported methods are displayed on simple keyring snap page', + ); + await this.driver.waitForSelector(this.accountSupportedMethods); + } + + async check_simpleKeyringSnapConnected(): Promise { + console.log('Check simple keyring snap is connected'); + await this.driver.waitForSelector(this.snapConnectedMessage); + } +} + +export default SnapSimpleKeyringPage; diff --git a/test/e2e/tests/account/remove-account-snap.spec.ts b/test/e2e/tests/account/remove-account-snap.spec.ts new file mode 100644 index 000000000000..2f0e2ab96a33 --- /dev/null +++ b/test/e2e/tests/account/remove-account-snap.spec.ts @@ -0,0 +1,50 @@ +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import FixtureBuilder from '../../fixture-builder'; +import { withFixtures, WINDOW_TITLES } from '../../helpers'; +import AccountListPage from '../../page-objects/pages/account-list-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import SnapListPage from '../../page-objects/pages/snap-list-page'; +import SnapSimpleKeyringPage from '../../page-objects/pages/snap-simple-keyring-page'; +import { installSnapSimpleKeyring } from '../../page-objects/flows/snap-simple-keyring.flow'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; + +describe('Remove Account Snap @no-mmi', function (this: Suite) { + it('disable a snap and remove it', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await installSnapSimpleKeyring(driver); + const snapSimpleKeyringPage = new SnapSimpleKeyringPage(driver); + await snapSimpleKeyringPage.createNewAccount(); + + // Check snap account is displayed after adding the snap account. + await driver.switchToWindowWithTitle( + WINDOW_TITLES.ExtensionInFullScreenView, + ); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.check_accountLabel('SSK Account'); + + // Navigate to account snaps list page. + await headerNavbar.openSnapListPage(); + const snapListPage = new SnapListPage(driver); + + // Remove the snap and check snap is successfully removed + await snapListPage.removeSnapByName('MetaMask Simple Snap Keyring'); + await snapListPage.check_noSnapInstalledMessageIsDisplayed(); + + // Assert that the snap account is removed from the account list + await headerNavbar.openAccountMenu(); + const accountListPage = new AccountListPage(driver); + await accountListPage.check_pageIsLoaded(); + await accountListPage.check_accountIsNotDisplayedInAccountList( + 'SSK Account', + ); + }, + ); + }); +}); From b7b3bddd59ba07606c1e0ff18d60294a317fcb6d Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:26:34 +0200 Subject: [PATCH 02/12] test: removing race condition for asserting inner values (PR-#1) (#27606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR fixes an anti-pattern in our e2e tests, where we assert that an element value is equal to a condition. This opens the door to a race condition where the element is already present, but not the value we want yet, making the assertion to fail. We should find the element by its value, instead of asserting its inner value. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27606?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/19870 Note: we will need several PRs to address all the occurrences, so we cannot close the issue yet, despite merging this first PR ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** n/a ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../e2e/tests/account/account-details.spec.js | 6 +- .../alerts/insufficient-funds.spec.ts | 19 +++-- .../tests/confirmations/navigation.spec.ts | 72 +++++++------------ .../signatures/malicious-signatures.spec.ts | 12 ++-- .../confirmations/signatures/permit.spec.ts | 54 +++++++------- .../signatures/personal-sign.spec.ts | 17 ++--- .../signatures/sign-typed-data-v3.spec.ts | 11 ++- .../signatures/sign-typed-data-v4.spec.ts | 15 ++-- .../signatures/sign-typed-data.spec.ts | 15 ++-- .../phishing-detection.spec.js | 18 ++--- .../tests/portfolio/portfolio-site.spec.js | 8 +-- test/e2e/tests/settings/terms-of-use.spec.js | 8 +-- 12 files changed, 108 insertions(+), 147 deletions(-) diff --git a/test/e2e/tests/account/account-details.spec.js b/test/e2e/tests/account/account-details.spec.js index a21b9fad2e7d..f40560cb27fd 100644 --- a/test/e2e/tests/account/account-details.spec.js +++ b/test/e2e/tests/account/account-details.spec.js @@ -60,8 +60,7 @@ describe('Show account details', function () { ); await driver.clickElement('[data-testid="account-list-menu-details"'); - const qrCode = await driver.findElement('.qr-code__wrapper'); - assert.equal(await qrCode.isDisplayed(), true); + await driver.waitForSelector('.qr-code__wrapper'); }, ); }); @@ -198,11 +197,10 @@ describe('Show account details', function () { await driver.press('#account-details-authenticate', driver.Key.ENTER); // Display error when password is incorrect - const passwordErrorIsDisplayed = await driver.isElementPresent({ + await driver.waitForSelector({ css: '.mm-help-text', text: 'Incorrect Password.', }); - assert.equal(passwordErrorIsDisplayed, true); }, ); }); diff --git a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts index b473dabe8478..59618596c344 100644 --- a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts +++ b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import FixtureBuilder from '../../../fixture-builder'; import { PRIVATE_KEY, @@ -52,8 +51,10 @@ describe('Alert for insufficient funds @no-mmi', function () { }); async function verifyAlertForInsufficientBalance(driver: Driver) { - const alert = await driver.findElement('[data-testid="inline-alert"]'); - assert.equal(await alert.getText(), 'Alert'); + await driver.waitForSelector({ + css: '[data-testid="inline-alert"]', + text: 'Alert', + }); await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); await driver.clickElement('[data-testid="inline-alert"]'); @@ -70,12 +71,8 @@ async function mintNft(driver: Driver) { } async function displayAlertForInsufficientBalance(driver: Driver) { - const alertDescription = await driver.findElement( - '[data-testid="alert-modal__selected-alert"]', - ); - const alertDescriptionText = await alertDescription.getText(); - assert.equal( - alertDescriptionText, - 'You do not have enough ETH in your account to pay for transaction fees.', - ); + await driver.waitForSelector({ + css: '[data-testid="alert-modal__selected-alert"]', + text: 'You do not have enough ETH in your account to pay for transaction fees.', + }); } diff --git a/test/e2e/tests/confirmations/navigation.spec.ts b/test/e2e/tests/confirmations/navigation.spec.ts index d6befdff0a26..747ba15872b3 100644 --- a/test/e2e/tests/confirmations/navigation.spec.ts +++ b/test/e2e/tests/confirmations/navigation.spec.ts @@ -1,4 +1,3 @@ -import { strict as assert } from 'assert'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import { Suite } from 'mocha'; import { By } from 'selenium-webdriver'; @@ -68,7 +67,7 @@ describe('Navigation Signature - Different signature types', function (this: Sui ); // Verify Transaction Sending ETH is displayed - await verifyTransaction(driver, 'SENDING ETH'); + await verifyTransaction(driver, 'Sending ETH'); await driver.clickElement('[data-testid="next-page"]'); @@ -80,7 +79,7 @@ describe('Navigation Signature - Different signature types', function (this: Sui ); // Verify Sign Typed Data v3 confirmation is displayed - await verifyTransaction(driver, 'SENDING ETH'); + await verifyTransaction(driver, 'Sending ETH'); await driver.clickElement('[data-testid="previous-page"]'); @@ -99,9 +98,10 @@ describe('Navigation Signature - Different signature types', function (this: Sui await openDapp(driver); await queueSignatures(driver); - await driver.clickElement('[data-testid="confirm-nav__reject-all"]'); + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-nav__reject-all"]', + ); - await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await verifyRejectionResults(driver, '#signTypedDataResult'); @@ -113,97 +113,79 @@ describe('Navigation Signature - Different signature types', function (this: Sui }); async function verifySignTypedData(driver: Driver) { - const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); - const message = await driver.findElement({ text: 'Hi, Alice!' }); - - // Verify Sign Typed Data confirmation is displayed - assert.ok(origin, 'origin'); - assert.ok(message, 'message'); + await driver.waitForSelector({ text: DAPP_HOST_ADDRESS }); + await driver.waitForSelector({ text: 'Hi, Alice!' }); } async function verifyRejectionResults(driver: Driver, verifyResultId: string) { - const rejectionResult = await driver.findElement(verifyResultId); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: verifyResultId, + text: 'Error: User rejected the request.', + }); } async function verifySignedTypeV3Confirmation(driver: Driver) { - const origin = await driver.findElement({ text: DAPP_HOST_ADDRESS }); - const fromAddress = driver.findElement({ + await driver.waitForSelector({ text: DAPP_HOST_ADDRESS }); + await driver.waitForSelector({ css: '.name__value', text: '0xCD2a3...DD826', }); - const toAddress = driver.findElement({ + await driver.waitForSelector({ css: '.name__value', text: '0xbBbBB...bBBbB', }); - const contents = driver.findElement({ text: 'Hello, Bob!' }); - - assert.ok(await origin, 'origin'); - assert.ok(await fromAddress, 'fromAddress'); - assert.ok(await toAddress, 'toAddress'); - assert.ok(await contents, 'contents'); + await driver.waitForSelector({ text: 'Hello, Bob!' }); } async function verifySignedTypeV4Confirmation(driver: Driver) { verifySignedTypeV3Confirmation(driver); - const attachment = driver.findElement({ text: '0x' }); - assert.ok(await attachment, 'attachment'); + await driver.waitForSelector({ text: '0x' }); } async function queueSignatures(driver: Driver) { // There is a race condition which changes the order in which signatures are displayed (#25251) // We fix it deterministically by waiting for an element in the screen for each signature await driver.clickElement('#signTypedData'); - await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Hi, Alice!' }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV3'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.findElement({ text: 'Reject all' }); await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 2']")); - await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV4'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } async function queueSignaturesAndTransactions(driver: Driver) { await driver.clickElement('#signTypedData'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); // Delay needed due to a race condition - // To be fixed in https://github.com/MetaMask/metamask-extension/issues/25251 - - await driver.waitUntilXWindowHandles(3); + await driver.waitForSelector({ + tag: 'p', + text: 'Hi, Alice!', + }); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#sendButton'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 2']")); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV3'); - await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); - await driver.delay(2000); + await driver.waitForSelector(By.xpath("//div[normalize-space(.)='1 of 3']")); } async function verifyTransaction( driver: Driver, expectedTransactionType: string, ) { - const transactionType = await driver.findElement( - '.confirm-page-container-summary__action__name', - ); - assert.equal(await transactionType.getText(), expectedTransactionType); + await driver.waitForSelector({ + tag: 'span', + text: expectedTransactionType, + }); } diff --git a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts index 69d8cbbc4f84..053c9f40f8b7 100644 --- a/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts +++ b/test/e2e/tests/confirmations/signatures/malicious-signatures.spec.ts @@ -152,8 +152,10 @@ async function acknowledgeAlert(driver: Driver) { async function verifyAlertIsDisplayed(driver: Driver) { await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); - const alert = await driver.findElement('[data-testid="inline-alert"]'); - assert.equal(await alert.getText(), 'Alert'); + await driver.waitForSelector({ + css: '[data-testid="inline-alert"]', + text: 'Alert', + }); await driver.clickElement('[data-testid="inline-alert"]'); } @@ -161,6 +163,8 @@ async function assertVerifiedMessage(driver: Driver, message: string) { await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const verifySigUtil = await driver.findElement('#siweResult'); - assert.equal(await verifySigUtil.getText(), message); + await driver.waitForSelector({ + css: '#siweResult', + text: message, + }); } diff --git a/test/e2e/tests/confirmations/signatures/permit.spec.ts b/test/e2e/tests/confirmations/signatures/permit.spec.ts index 2a87db442da5..c9c4ca9399f4 100644 --- a/test/e2e/tests/confirmations/signatures/permit.spec.ts +++ b/test/e2e/tests/confirmations/signatures/permit.spec.ts @@ -94,11 +94,10 @@ describe('Confirmation Signature - Permit @no-mmi', function (this: Suite) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement('#signPermitResult'); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + tag: 'span', + text: 'Error: User rejected the request.', + }); await assertSignatureRejectedMetrics({ driver, @@ -146,31 +145,32 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signPermitVerify'); - const verifyResult = await driver.findElement('#signPermitResult'); - const verifyResultR = await driver.findElement('#signPermitResultR'); - const verifyResultS = await driver.findElement('#signPermitResultS'); - const verifyResultV = await driver.findElement('#signPermitResultV'); + await driver.waitForSelector({ + css: '#signPermitVerifyResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#signPermitResult', + text: '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', + }); + await driver.waitForSelector({ + css: '#signPermitResultR', + text: 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', + }); + + await driver.waitForSelector({ + css: '#signPermitResultS', + text: 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', + }); + + await driver.waitForSelector({ + css: '#signPermitResultV', + text: 'v: 28', + }); await driver.waitForSelector({ css: '#signPermitVerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signPermitVerifyResult', - ); - - assert.equal( - await verifyResult.getText(), - '0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee1c', - ); - assert.equal( - await verifyResultR.getText(), - 'r: 0x0a396f89ee073214f7e055e700048abd7b4aba6ecca0352937d6a2ebb7176f2f', - ); - assert.equal( - await verifyResultS.getText(), - 's: 0x43c63097ad7597632e34d6a801695702ba603d5872a33ee7d7562fcdb9e816ee', - ); - assert.equal(await verifyResultV.getText(), 'v: 28'); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts index 429881bf2f23..dca5e6ba27d5 100644 --- a/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts +++ b/test/e2e/tests/confirmations/signatures/personal-sign.spec.ts @@ -116,17 +116,18 @@ async function assertVerifiedPersonalMessage( await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#personalSignVerify'); - const verifySigUtil = await driver.findElement( - '#personalSignVerifySigUtilResult', - ); await driver.waitForSelector({ css: '#personalSignVerifyECRecoverResult', text: publicAddress, }); - const verifyECRecover = await driver.findElement( - '#personalSignVerifyECRecoverResult', - ); - assert.equal(await verifySigUtil.getText(), publicAddress); - assert.equal(await verifyECRecover.getText(), publicAddress); + await driver.waitForSelector({ + css: '#personalSignVerifySigUtilResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#personalSignVerifyECRecoverResult', + text: publicAddress, + }); } diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts index 0eb1d2c698b1..f2c62e617899 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v3.spec.ts @@ -96,13 +96,10 @@ describe('Confirmation Signature - Sign Typed Data V3 @no-mmi', function (this: await driver.waitUntilXWindowHandles(2); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); - const rejectionResult = await driver.findElement( - '#signTypedDataV3Result', - ); - assert.equal( - await rejectionResult.getText(), - 'Error: User rejected the request.', - ); + await driver.waitForSelector({ + css: '#signTypedDataV3Result', + text: 'Error: User rejected the request.', + }); }, mockSignatureRejected, ); diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts index d78acb511ce9..ca0dbb8f9bb6 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data-v4.spec.ts @@ -154,18 +154,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataV4Verify'); - const verifyResult = await driver.findElement('#signTypedDataV4Result'); + await driver.waitForSelector({ + css: '#signTypedDataV4Result', + text: '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', + }); + await driver.waitForSelector({ css: '#signTypedDataV4VerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataV4VerifyResult', - ); - - assert.equal( - await verifyResult.getText(), - '0xcd2f9c55840f5e1bcf61812e93c1932485b524ca673b36355482a4fbdf52f692684f92b4f4ab6f6c8572dacce46bd107da154be1c06939b855ecce57a1616ba71b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts index 358d6b112cfc..a9a9dfd52ae9 100644 --- a/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts +++ b/test/e2e/tests/confirmations/signatures/sign-typed-data.spec.ts @@ -116,18 +116,13 @@ async function assertVerifiedResults(driver: Driver, publicAddress: string) { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.clickElement('#signTypedDataVerify'); - const result = await driver.findElement('#signTypedDataResult'); + await driver.waitForSelector({ + css: '#signTypedDataResult', + text: '0x32791e3c41d40dd5bbfb42e66cf80ca354b0869ae503ad61cd19ba68e11d4f0d2e42a5835b0bfd633596b6a7834ef7d36033633a2479dacfdb96bda360d51f451b', + }); + await driver.waitForSelector({ css: '#signTypedDataVerifyResult', text: publicAddress, }); - const verifyRecoverAddress = await driver.findElement( - '#signTypedDataVerifyResult', - ); - - assert.equal( - await result.getText(), - '0x32791e3c41d40dd5bbfb42e66cf80ca354b0869ae503ad61cd19ba68e11d4f0d2e42a5835b0bfd633596b6a7834ef7d36033633a2479dacfdb96bda360d51f451b', - ); - assert.equal(await verifyRecoverAddress.getText(), publicAddress); } diff --git a/test/e2e/tests/phishing-controller/phishing-detection.spec.js b/test/e2e/tests/phishing-controller/phishing-detection.spec.js index 444c98900026..ac9a6d8461d2 100644 --- a/test/e2e/tests/phishing-controller/phishing-detection.spec.js +++ b/test/e2e/tests/phishing-controller/phishing-detection.spec.js @@ -208,10 +208,9 @@ describe('Phishing Detection', function () { await driver.findElement({ text: `Empty page by ${BlockProvider.MetaMask}`, }); - assert.equal( - await driver.getCurrentUrl(), - `https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%2F`, - ); + await driver.waitForUrl({ + url: `https://github.com/MetaMask/eth-phishing-detect/issues/new?title=[Legitimate%20Site%20Blocked]%20127.0.0.1&body=http%3A%2F%2F127.0.0.1%2F`, + }); }, ); }); @@ -445,11 +444,12 @@ describe('Phishing Detection', function () { await driver.openNewURL(blockedUrl); // check that the redirect was ultimately _not_ followed and instead // went to our "MetaMask Phishing Detection" site - assert.equal( - await driver.getCurrentUrl(), - // http://localhost:9999 is the Phishing Warning page - `http://localhost:9999/#hostname=${blocked}&href=http%3A%2F%2F${blocked}%3A${port}%2F`, - ); + + await driver.waitForUrl({ + url: + // http://localhost:9999 is the Phishing Warning page + `http://localhost:9999/#hostname=${blocked}&href=http%3A%2F%2F${blocked}%3A${port}%2F`, + }); }); } }); diff --git a/test/e2e/tests/portfolio/portfolio-site.spec.js b/test/e2e/tests/portfolio/portfolio-site.spec.js index ff4c7c363a71..cba9c0452522 100644 --- a/test/e2e/tests/portfolio/portfolio-site.spec.js +++ b/test/e2e/tests/portfolio/portfolio-site.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { withFixtures, unlockWallet, @@ -42,10 +41,9 @@ describe('Portfolio site', function () { await driver.switchToWindowWithTitle('E2E Test Page', windowHandles); // Verify site - const currentUrl = await driver.getCurrentUrl(); - const expectedUrl = - 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null&metricsEnabled=false&marketingEnabled=false'; - assert.equal(currentUrl, expectedUrl); + await driver.waitForUrl({ + url: 'https://portfolio.metamask.io/?metamaskEntry=ext_portfolio_button&metametricsId=null&metricsEnabled=false&marketingEnabled=false', + }); }, ); }); diff --git a/test/e2e/tests/settings/terms-of-use.spec.js b/test/e2e/tests/settings/terms-of-use.spec.js index 87c2c2d0018d..ee314ee95600 100644 --- a/test/e2e/tests/settings/terms-of-use.spec.js +++ b/test/e2e/tests/settings/terms-of-use.spec.js @@ -1,4 +1,3 @@ -const { strict: assert } = require('assert'); const { defaultGanacheOptions, withFixtures, @@ -26,12 +25,7 @@ describe('Terms of use', function () { const acceptTerms = '[data-testid="terms-of-use-accept-button"]'; await driver.clickElement('[data-testid="popover-scroll-button"]'); await driver.clickElement('[data-testid="terms-of-use-checkbox"]'); - await driver.clickElement(acceptTerms); - - // check modal is no longer shown - await driver.assertElementNotPresent(acceptTerms); - const termsExists = await driver.isElementPresent(acceptTerms); - assert.equal(termsExists, false, 'terms of use should not be shown'); + await driver.clickElementAndWaitToDisappear(acceptTerms); }, ); }); From 98c067607b4212658c0dc07184e17baf544c39c4 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 7 Oct 2024 14:19:51 +0100 Subject: [PATCH 03/12] fix: Design papercuts for redesigned transactions (#27605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Addresses QA and design reviews, including various spacing issues, and a correction on the loader behaviour for the confirmations. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27605?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3402 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../confirmations/signatures/permit.test.tsx | 26 +- .../signatures/personalSign.test.tsx | 26 +- .../row/__snapshots__/address.test.tsx.snap | 4 +- .../expandable-row.test.tsx.snap | 2 +- .../info/row/__snapshots__/row.test.tsx.snap | 4 +- .../__snapshots__/alert-row.test.tsx.snap | 2 +- ui/components/app/confirm/info/row/row.tsx | 1 + .../app/name/__snapshots__/name.test.tsx.snap | 16 +- .../__snapshots__/name-details.test.tsx.snap | 20 +- ui/components/app/name/name.tsx | 8 +- .../info/__snapshots__/info.test.tsx.snap | 266 ++------- .../__snapshots__/approve.test.tsx.snap | 34 +- .../approve-details.test.tsx.snap | 4 +- .../approve-details/approve-details.tsx | 5 + .../approve-static-simulation.test.tsx.snap | 8 +- .../confirm/info/approve/approve.tsx | 7 +- .../edit-spending-cap-modal.test.tsx.snap | 2 +- .../edit-spending-cap-modal.tsx | 24 +- .../__snapshots__/spending-cap.test.tsx.snap | 4 +- .../base-transaction-info.test.tsx.snap | 24 +- .../components/confirm/info/info.test.tsx | 16 +- .../__snapshots__/personal-sign.test.tsx.snap | 8 +- .../__snapshots__/siwe-sign.test.tsx.snap | 38 +- .../set-approval-for-all-info.test.tsx.snap | 80 +-- ...al-for-all-static-simulation.test.tsx.snap | 14 +- .../set-approval-for-all-info.test.tsx | 5 + .../set-approval-for-all-info.tsx | 7 +- ...al-for-all-static-simulation.test.tsx.snap | 8 +- .../advanced-details.test.tsx.snap | 8 +- .../confirm-loader.test.tsx.snap | 49 ++ .../confirm-loader/confirm-loader.test.tsx | 23 + .../shared/confirm-loader/confirm-loader.tsx | 20 + .../edit-gas-fees-row.test.tsx.snap | 2 +- .../gas-fees-details.test.tsx.snap | 4 +- .../__snapshots__/gas-fees-row.test.tsx.snap | 2 +- .../gas-fees-section.test.tsx.snap | 4 +- .../transaction-data.test.tsx.snap | 96 ++-- .../transaction-details.test.tsx.snap | 4 +- .../__snapshots__/typed-sign-v1.test.tsx.snap | 8 +- .../__snapshots__/typed-sign.test.tsx.snap | 150 ++--- .../permit-simulation.test.tsx.snap | 8 +- .../__snapshots__/value-display.test.tsx.snap | 4 +- .../row/__snapshots__/dataTree.test.tsx.snap | 56 +- .../typedSignDataV1.test.tsx.snap | 4 +- .../__snapshots__/typedSignData.test.tsx.snap | 30 +- .../title/hooks/useCurrentSpendingCap.ts | 4 +- .../components/confirm/title/title.test.tsx | 19 +- .../components/confirm/title/title.tsx | 70 ++- .../__snapshots__/confirm.test.tsx.snap | 542 ++++++++++++++---- .../confirmations/confirm/confirm.test.tsx | 41 +- 50 files changed, 1045 insertions(+), 766 deletions(-) create mode 100644 ui/pages/confirmations/components/confirm/info/shared/confirm-loader/__snapshots__/confirm-loader.test.tsx.snap create mode 100644 ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx create mode 100644 ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index 809ac988962f..e11f206d1996 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -1,16 +1,16 @@ -import { act, fireEvent, waitFor, screen } from '@testing-library/react'; -import nock from 'nock'; import { ApprovalType } from '@metamask/controller-utils'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import { shortenAddress } from '../../../../ui/helpers/utils/util'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import nock from 'nock'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; import { createMockImplementation } from '../../helpers'; jest.mock('../../../../ui/store/background-connection', () => ({ @@ -182,10 +182,12 @@ describe('Permit Confirmation', () => { }); }); - expect(screen.getByText('Spending cap request')).toBeInTheDocument(); - expect( - screen.getByText('This site wants permission to spend your tokens.'), - ).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('Spending cap request')).toBeInTheDocument(); + expect( + screen.getByText('This site wants permission to spend your tokens.'), + ).toBeInTheDocument(); + }); }); it('displays the simulation section', async () => { diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx index 5a965a3d6928..690446caa533 100644 --- a/test/integration/confirmations/signatures/personalSign.test.tsx +++ b/test/integration/confirmations/signatures/personalSign.test.tsx @@ -1,15 +1,15 @@ -import { fireEvent, waitFor } from '@testing-library/react'; import { ApprovalType } from '@metamask/controller-utils'; -import mockMetaMaskState from '../../data/integration-init-state.json'; -import { integrationTestRender } from '../../../lib/render-helpers'; -import { shortenAddress } from '../../../../ui/helpers/utils/util'; -import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, - MetaMetricsEventName, MetaMetricsEventLocation, + MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { MESSAGE_TYPE } from '../../../../shared/constants/app'; +import { shortenAddress } from '../../../../ui/helpers/utils/util'; +import * as backgroundConnection from '../../../../ui/store/background-connection'; +import { integrationTestRender } from '../../../lib/render-helpers'; +import mockMetaMaskState from '../../data/integration-init-state.json'; jest.mock('../../../../ui/store/background-connection', () => ({ ...jest.requireActual('../../../../ui/store/background-connection'), @@ -156,14 +156,16 @@ describe('PersonalSign Confirmation', () => { account.address, ); - const { getByText } = await integrationTestRender({ - preloadedState: mockedMetaMaskState, - backgroundConnection: backgroundConnectionMocked, + await act(async () => { + await integrationTestRender({ + preloadedState: mockedMetaMaskState, + backgroundConnection: backgroundConnectionMocked, + }); }); - expect(getByText('Signature request')).toBeInTheDocument(); + expect(screen.getByText('Signature request')).toBeInTheDocument(); expect( - getByText('Review request details before you confirm.'), + screen.getByText('Review request details before you confirm.'), ).toBeInTheDocument(); }); diff --git a/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap index 292be0318ce6..ec2eacb0d44b 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/address.test.tsx.snap @@ -315,7 +315,9 @@ exports[`ConfirmInfoRowAddress renders appropriately with PetNames enabled 1`] =
-
+
diff --git a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap index b11a8d89bd87..c3958d886710 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/expandable-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ConfirmInfoExpandableRow should match snapshot 1`] = `
= ({ flexDirection={FlexDirection.Row} justifyContent={JustifyContent.spaceBetween} flexWrap={FlexWrap.Wrap} + alignItems={AlignItems.center} backgroundColor={BACKGROUND_COLORS[variant]} borderRadius={BorderRadius.LG} marginTop={2} diff --git a/ui/components/app/name/__snapshots__/name.test.tsx.snap b/ui/components/app/name/__snapshots__/name.test.tsx.snap index 63198ff44507..379c00faab11 100644 --- a/ui/components/app/name/__snapshots__/name.test.tsx.snap +++ b/ui/components/app/name/__snapshots__/name.test.tsx.snap @@ -2,7 +2,9 @@ exports[`Name renders address with image 1`] = `
-
+
@@ -24,7 +26,9 @@ exports[`Name renders address with image 1`] = ` exports[`Name renders address with no saved name 1`] = `
-
+
@@ -44,7 +48,9 @@ exports[`Name renders address with no saved name 1`] = ` exports[`Name renders address with saved name 1`] = `
-
+
@@ -104,7 +110,9 @@ exports[`Name renders address with saved name 1`] = ` exports[`Name renders when no address value is passed 1`] = `
-
+
diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap index 6ee9430c0fde..a6d0df79843d 100644 --- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap +++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap @@ -66,7 +66,9 @@ exports[`NameDetails renders proposed names 1`] = `
-
+
@@ -326,7 +328,9 @@ exports[`NameDetails renders when no address value is passed 1`] = `
-
+
@@ -509,7 +513,9 @@ exports[`NameDetails renders with no saved name 1`] = `
-
+
@@ -694,7 +700,9 @@ exports[`NameDetails renders with recognized name 1`] = `
-
+
@@ -884,7 +892,9 @@ exports[`NameDetails renders with saved name 1`] = `
-
+
diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx index d2684c188838..5af2851c8885 100644 --- a/ui/components/app/name/name.tsx +++ b/ui/components/app/name/name.tsx @@ -8,14 +8,14 @@ import React, { import { NameType } from '@metamask/name-controller'; import classnames from 'classnames'; import { toChecksumAddress } from 'ethereumjs-util'; -import { Icon, IconName, IconSize, Text } from '../../component-library'; +import { Box, Icon, IconName, IconSize, Text } from '../../component-library'; import { shortenAddress } from '../../../helpers/utils/util'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { TextVariant } from '../../../helpers/constants/design-system'; +import { Display, TextVariant } from '../../../helpers/constants/design-system'; import { useDisplayName } from '../../../hooks/useDisplayName'; import Identicon from '../../ui/identicon'; import NameDetails from './name-details/name-details'; @@ -98,7 +98,7 @@ const Name = memo( const hasDisplayName = Boolean(name); return ( -
+ {!disableEdit && modalOpen && ( )} @@ -130,7 +130,7 @@ const Name = memo( )}
-
+ ); }, ); diff --git a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap index c13f6bc4a695..669dc0d5302d 100644 --- a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap @@ -2,144 +2,12 @@ exports[`Info renders info section for approve request 1`] = `
-
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -188,7 +56,7 @@ exports[`Info renders info section for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -252,7 +120,7 @@ exports[`Info renders info section for approve request 1`] = `
@@ -384,7 +252,7 @@ exports[`Info renders info section for contract interaction request 1`] = ` data-testid="transaction-details-section" >
@@ -428,7 +296,7 @@ exports[`Info renders info section for contract interaction request 1`] = `
@@ -525,7 +393,7 @@ exports[`Info renders info section for contract interaction request 1`] = ` data-testid="gas-fee-section" >
@@ -589,7 +457,7 @@ exports[`Info renders info section for contract interaction request 1`] = `
@@ -640,7 +508,7 @@ exports[`Info renders info section for personal sign request 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -824,73 +694,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = ` data-testid="confirmation__approve-details" >
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -939,7 +743,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = ` data-testid="gas-fee-section" >
@@ -1003,7 +807,7 @@ exports[`Info renders info section for setApprovalForAll request 1`] = `
@@ -1054,7 +858,7 @@ exports[`Info renders info section for typed sign request 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
renders component for approve request 1`] = ` data-testid="confirmation__simulation_section" >
renders component for approve request 1`] = `
renders component for approve request 1`] = ` 1000

-
+
@@ -107,7 +109,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-details" >
@@ -202,7 +204,7 @@ exports[` renders component for approve request 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -246,7 +248,7 @@ exports[` renders component for approve request 1`] = `
@@ -343,7 +345,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-spending-cap-section" >
renders component for approve request 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -435,7 +437,7 @@ exports[` renders component for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -499,7 +501,7 @@ exports[` renders component for approve request 1`] = `
@@ -541,7 +543,7 @@ exports[` renders component for approve request 1`] = `
@@ -595,7 +597,7 @@ exports[` renders component for approve request 1`] = ` data-testid="advanced-details-nonce-section" >
renders component for approve request 1`] = ` data-testid="advanced-details-data-section" >
renders component for approve request 1`] = ` />
@@ -699,7 +701,7 @@ exports[` renders component for approve request 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -791,7 +793,7 @@ exports[` renders component for approve request 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap index a66499aab561..17d04e237fb2 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/approve-details/__snapshots__/approve-details.test.tsx.snap @@ -11,7 +11,7 @@ exports[` renders component for approve details 1`] = ` data-testid="advanced-details-data-section" >
renders component for approve details for setApprova data-testid="advanced-details-data-section" >
renders component 1`] = ` data-testid="confirmation__simulation_section" >
renders component 1`] = `
renders component 1`] = ` 1000

-
+
diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx index a982c8af90bb..eabf8639ccfb 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -8,6 +8,7 @@ import { useConfirmContext } from '../../../../context/confirm'; import { useAssetDetails } from '../../../../hooks/useAssetDetails'; import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/preferences'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import { ApproveDetails } from './approve-details/approve-details'; import { ApproveStaticSimulation } from './approve-static-simulation/approve-static-simulation'; @@ -38,7 +39,7 @@ const ApproveInfo = () => { transactionMeta.txParams.data, ); - const { spendingCap } = useApproveTokenSimulation( + const { spendingCap, pending } = useApproveTokenSimulation( transactionMeta, decimals || '0', ); @@ -51,6 +52,10 @@ const ApproveInfo = () => { return null; } + if (pending) { + return ; + } + return ( <> {showRevokeVariant ? ( diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap index 7d64def9ff73..e56a87ed34d7 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/__snapshots__/edit-spending-cap-modal.test.tsx.snap @@ -57,7 +57,7 @@ exports[` renders component 1`] = ` focused="true" placeholder="1000 TST" type="number" - value="" + value="1000" />

{ + if (formattedSpendingCap) { + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + } + }, [formattedSpendingCap]); const handleCancel = useCallback(() => { setIsOpenEditSpendingCapModal(false); - setCustomSpendingCapInputValue(''); - }, [setIsOpenEditSpendingCapModal, setCustomSpendingCapInputValue]); + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + }, [ + setIsOpenEditSpendingCapModal, + setCustomSpendingCapInputValue, + formattedSpendingCap, + ]); const [isModalSaving, setIsModalSaving] = useState(false); const handleSubmit = useCallback(async () => { setIsModalSaving(true); - const parsedValue = parseInt(customSpendingCapInputValue, 10); + const parsedValue = parseInt(String(customSpendingCapInputValue), 10); const customTxParamsData = getCustomTxParamsData( transactionMeta?.txParams?.data, @@ -103,8 +113,8 @@ export const EditSpendingCapModal = ({ setIsModalSaving(false); setIsOpenEditSpendingCapModal(false); - setCustomSpendingCapInputValue(''); - }, [customSpendingCapInputValue]); + setCustomSpendingCapInputValue(formattedSpendingCap.toString()); + }, [customSpendingCapInputValue, formattedSpendingCap]); return ( renders component 1`] = ` data-testid="confirmation__approve-spending-cap-section" >

renders component 1`] = ` style="height: 1px; margin-left: -8px; margin-right: -8px;" />
diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap index efc77a1b8eb1..5f0370343f00 100644 --- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap @@ -89,7 +89,7 @@ exports[` renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -133,7 +133,7 @@ exports[` renders component for contract interaction requ
@@ -230,7 +230,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -294,7 +294,7 @@ exports[` renders component for contract interaction requ
@@ -464,7 +464,7 @@ exports[` renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -508,7 +508,7 @@ exports[` renders component for contract interaction requ
@@ -605,7 +605,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -669,7 +669,7 @@ exports[` renders component for contract interaction requ
@@ -836,7 +836,7 @@ exports[` renders component for contract interaction requ data-testid="transaction-details-section" >
@@ -880,7 +880,7 @@ exports[` renders component for contract interaction requ
@@ -977,7 +977,7 @@ exports[` renders component for contract interaction requ data-testid="gas-fee-section" >
@@ -1041,7 +1041,7 @@ exports[` renders component for contract interaction requ
diff --git a/ui/pages/confirmations/components/confirm/info/info.test.tsx b/ui/pages/confirmations/components/confirm/info/info.test.tsx index 4931c2fbaa01..75d98d91a1bd 100644 --- a/ui/pages/confirmations/components/confirm/info/info.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/info.test.tsx @@ -1,6 +1,6 @@ +import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; - import { getMockApproveConfirmState, getMockContractInteractionConfirmState, @@ -50,17 +50,27 @@ describe('Info', () => { expect(container).toMatchSnapshot(); }); - it('renders info section for approve request', () => { + it('renders info section for approve request', async () => { const state = getMockApproveConfirmState(); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider(, mockStore); + + await waitFor(() => { + expect(screen.getByText('Speed')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); - it('renders info section for setApprovalForAll request', () => { + it('renders info section for setApprovalForAll request', async () => { const state = getMockSetApprovalForAllConfirmState(); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider(, mockStore); + + await waitFor(() => { + expect(screen.getByText('Speed')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap index 79290bb3b49b..2f46c283b830 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/__snapshots__/personal-sign.test.tsx.snap @@ -6,7 +6,7 @@ exports[`PersonalSignInfo handle reverse string properly 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
renders component for approve request 1`] = ` data-testid="confirmation__simulation_section" >
renders component for approve request 1`] = `
renders component for approve request 1`] = ` All

-
+
@@ -109,73 +111,7 @@ exports[` renders component for approve request 1`] = ` data-testid="confirmation__approve-details" >
-
-
-
-

- Data -

-
-
-
- - - - - - - - - -
-
-
-
@@ -224,7 +160,7 @@ exports[` renders component for approve request 1`] = ` data-testid="gas-fee-section" >
@@ -288,7 +224,7 @@ exports[` renders component for approve request 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap index 37e785040b0f..8c94061320fe 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/revoke-set-approval-for-all-static-simulation/__snapshots__/revoke-set-approval-for-all-static-simulation.test.tsx.snap @@ -7,7 +7,7 @@ exports[` renders component for setAp data-testid="confirmation__simulation_section" >
renders component for setAp
renders component for setAp
-
+
@@ -92,7 +94,7 @@ exports[` renders component for setAp
renders component for setAp
-
+
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx index 3460b1d8e76e..77a840dcece5 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.test.tsx @@ -1,3 +1,4 @@ +import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -39,6 +40,10 @@ describe('', () => { mockStore, ); + await waitFor(() => { + expect(screen.getByText('Data')).toBeInTheDocument(); + }); + expect(container).toMatchSnapshot(); }); diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx index 6a2c98f224e2..92df913783a1 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-info.tsx @@ -6,6 +6,7 @@ import { selectConfirmationAdvancedDetailsOpen } from '../../../../selectors/pre import { ApproveDetails } from '../approve/approve-details/approve-details'; import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData'; import { AdvancedDetails } from '../shared/advanced-details/advanced-details'; +import { ConfirmLoader } from '../shared/confirm-loader/confirm-loader'; import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section'; import { getIsRevokeSetApprovalForAll } from '../utils'; import { RevokeSetApprovalForAllStaticSimulation } from './revoke-set-approval-for-all-static-simulation/revoke-set-approval-for-all-static-simulation'; @@ -21,7 +22,7 @@ const SetApprovalForAllInfo = () => { const decodedResponse = useDecodedTransactionData(); - const { value } = decodedResponse; + const { value, pending } = decodedResponse; const isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll(value); @@ -31,6 +32,10 @@ const SetApprovalForAllInfo = () => { return null; } + if (pending) { + return ; + } + return ( <> {isRevokeSetApprovalForAll ? ( diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap index 9ab4107ff173..b2f289875f6b 100644 --- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/set-approval-for-all-static-simulation/__snapshots__/set-approval-for-all-static-simulation.test.tsx.snap @@ -7,7 +7,7 @@ exports[` renders component for approve req data-testid="confirmation__simulation_section" >
renders component for approve req
renders component for approve req All

-
+
diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap index a521ff23795a..f66db615defe 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/__snapshots__/advanced-details.test.tsx.snap @@ -7,7 +7,7 @@ exports[` does not render component for advanced transaction data-testid="advanced-details-nonce-section" >
does not render component for advanced transaction data-testid="advanced-details-data-section" >
renders component for advanced transaction details data-testid="advanced-details-nonce-section" >
renders component for advanced transaction details data-testid="advanced-details-data-section" >
renders component 1`] = ` +
+
+ + + + + + + + + +
+
+`; diff --git a/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx new file mode 100644 index 000000000000..155f6a27850b --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { getMockSetApprovalForAllConfirmState } from '../../../../../../../../test/data/confirmations/helper'; +import { renderWithConfirmContextProvider } from '../../../../../../../../test/lib/confirmations/render-helpers'; +import { ConfirmLoader } from './confirm-loader'; + +describe('', () => { + const middleware = [thunk]; + + it('renders component', async () => { + const state = getMockSetApprovalForAllConfirmState(); + + const mockStore = configureMockStore(middleware)(state); + + const { container } = renderWithConfirmContextProvider( + , + mockStore, + ); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx new file mode 100644 index 000000000000..2aa57f489dfe --- /dev/null +++ b/ui/pages/confirmations/components/confirm/info/shared/confirm-loader/confirm-loader.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Box } from '../../../../../../../components/component-library'; +import Preloader from '../../../../../../../components/ui/icon/preloader'; +import { + AlignItems, + Display, + JustifyContent, +} from '../../../../../../../helpers/constants/design-system'; + +export const ConfirmLoader = () => { + return ( + + + + ); +}; diff --git a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap index 87a78c4928bc..3ad4343e8ee5 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/edit-gas-fees-row/__snapshots__/edit-gas-fees-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders component 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-details/__snapshots__/gas-fees-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-details/__snapshots__/gas-fees-details.test.tsx.snap index d94bfe53860a..4941dcd656d9 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-details/__snapshots__/gas-fees-details.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-details/__snapshots__/gas-fees-details.test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders component for gas fees section 1`] = `
@@ -67,7 +67,7 @@ exports[` renders component for gas fees section 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap index 6643621734f6..5de2d2361b38 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/gas-fees-row/__snapshots__/gas-fees-row.test.tsx.snap @@ -3,7 +3,7 @@ exports[` renders component 1`] = `
renders component for gas fees section 1`] = ` data-testid="gas-fee-section" >
@@ -73,7 +73,7 @@ exports[` renders component for gas fees section 1`] = `
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap index b830449ca55c..c53805c877e1 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap @@ -7,7 +7,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = data-testid="advanced-details-data-section" >
@@ -140,7 +140,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -191,7 +191,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -299,7 +299,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -344,7 +344,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -389,7 +389,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -511,7 +511,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -562,7 +562,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -670,7 +670,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -726,7 +726,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -777,7 +777,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] = style="height: 1px; margin-left: -8px; margin-right: -8px;" />
@@ -885,7 +885,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -941,7 +941,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
@@ -999,7 +999,7 @@ exports[`TransactionData renders decoded data with no names 1`] = ` data-testid="advanced-details-data-section" >
@@ -1056,7 +1056,7 @@ exports[`TransactionData renders decoded data with no names 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1101,7 +1101,7 @@ exports[`TransactionData renders decoded data with no names 1`] = `
@@ -1157,7 +1157,7 @@ exports[`TransactionData renders decoded data with no names 1`] = `
@@ -1213,7 +1213,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` data-testid="advanced-details-data-section" >
@@ -1286,7 +1286,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1342,7 +1342,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1380,7 +1380,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1418,7 +1418,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1456,7 +1456,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1512,7 +1512,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1557,7 +1557,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1602,7 +1602,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1648,7 +1648,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1686,7 +1686,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1742,7 +1742,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1787,7 +1787,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1832,7 +1832,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1878,7 +1878,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -1916,7 +1916,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = ` class="mm-box mm-box--padding-left-2" >
@@ -1972,7 +1972,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2017,7 +2017,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2062,7 +2062,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2109,7 +2109,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2165,7 +2165,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2211,7 +2211,7 @@ exports[`TransactionData renders decoded data with tuples and arrays 1`] = `
@@ -2269,7 +2269,7 @@ exports[`TransactionData renders raw hexadecimal if no decoded data 1`] = ` data-testid="advanced-details-data-section" >
renders component for transaction details 1`] = data-testid="transaction-details-section" >
@@ -60,7 +60,7 @@ exports[` renders component for transaction details 1`] =
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap index 57aa7e62fcb2..59a6065e6b9d 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap @@ -6,7 +6,7 @@ exports[`TypedSignInfo correctly renders typed sign data request 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -127,7 +129,7 @@ exports[`TypedSignInfo correctly renders permit sign type 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
@@ -762,7 +766,7 @@ exports[`TypedSignInfo correctly renders permit sign type with no deadline 1`] = class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
-
+
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap index 022ff8b6dbc2..26def806c6fa 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/__snapshots__/value-display.test.tsx.snap @@ -32,7 +32,9 @@ exports[`PermitSimulationValueDisplay renders component correctly 1`] = `
-
+
diff --git a/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap b/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap index cc8f3451676a..68d8aab887be 100644 --- a/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/row/__snapshots__/dataTree.test.tsx.snap @@ -6,7 +6,7 @@ exports[`DataTree correctly renders reverse strings 1`] = ` class="mm-box mm-box--width-full" >
{ ).toBeInTheDocument(); }); - it('should render the title and description for a setApprovalForAll transaction', () => { + it('should render the title and description for a setApprovalForAll transaction', async () => { const mockStore = configureMockStore([])( getMockSetApprovalForAllConfirmState(), ); @@ -144,12 +144,15 @@ describe('ConfirmTitle', () => { mockStore, ); - expect( - getByText(tEn('setApprovalForAllRedesignedTitle') as string), - ).toBeInTheDocument(); - expect( - getByText(tEn('confirmTitleDescApproveTransaction') as string), - ).toBeInTheDocument(); + await waitFor(() => { + expect( + getByText(tEn('setApprovalForAllRedesignedTitle') as string), + ).toBeInTheDocument(); + + expect( + getByText(tEn('confirmTitleDescApproveTransaction') as string), + ).toBeInTheDocument(); + }); }); describe('Alert banner', () => { diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 34c66740c041..702c496b4e25 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -68,7 +68,12 @@ const getTitle = ( isNFT?: boolean, customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return t('confirmTitleTransaction'); @@ -109,7 +114,12 @@ const getDescription = ( isNFT?: boolean, customSpendingCap?: string, isRevokeSetApprovalForAll?: boolean, + pending?: boolean, ) => { + if (pending) { + return ''; + } + switch (confirmation?.type) { case TransactionType.contractInteraction: return ''; @@ -151,9 +161,11 @@ const ConfirmTitle: React.FC = memo(() => { const { isNFT } = useIsNFT(currentConfirmation as TransactionMeta); - const { customSpendingCap } = useCurrentSpendingCap(currentConfirmation); + const { customSpendingCap, pending: spendingCapPending } = + useCurrentSpendingCap(currentConfirmation); let isRevokeSetApprovalForAll = false; + let revokePending = false; if ( currentConfirmation?.type === TransactionType.tokenMethodSetApprovalForAll ) { @@ -162,6 +174,7 @@ const ConfirmTitle: React.FC = memo(() => { isRevokeSetApprovalForAll = getIsRevokeSetApprovalForAll( decodedResponse.value, ); + revokePending = decodedResponse.pending; } const title = useMemo( @@ -172,8 +185,16 @@ const ConfirmTitle: React.FC = memo(() => { isNFT, customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, ), - [currentConfirmation, isNFT, customSpendingCap, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + ], ); const description = useMemo( @@ -184,9 +205,16 @@ const ConfirmTitle: React.FC = memo(() => { isNFT, customSpendingCap, isRevokeSetApprovalForAll, + spendingCapPending || revokePending, ), - - [currentConfirmation, isNFT, customSpendingCap, isRevokeSetApprovalForAll], + [ + currentConfirmation, + isNFT, + customSpendingCap, + isRevokeSetApprovalForAll, + spendingCapPending, + revokePending, + ], ); if (!currentConfirmation) { @@ -196,21 +224,25 @@ const ConfirmTitle: React.FC = memo(() => { return ( <> - - {title} - - - {description} - + {title !== '' && ( + + {title} + + )} + {description !== '' && ( + + {description} + + )} ); }); diff --git a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap index 30a1b6ad118c..1d27b332f7a5 100644 --- a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap +++ b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap @@ -24,15 +24,48 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = `
+ > +
+ + + + + +
+
- Goerli logo + G

Signature request

@@ -105,7 +138,7 @@ exports[`Confirm matches snapshot for signature - personal sign type 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -340,7 +373,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB data-testid="confirmation__simulation_section" >
-
+
@@ -486,7 +521,9 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB
-
+
@@ -514,7 +551,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -1446,7 +1483,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitS data-testid="confirmation__simulation_section" >
-
+
@@ -1570,7 +1609,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitS class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
+ > +
+ + + + + +
+
- Goerli logo + G

Signature request

@@ -2254,7 +2326,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Spending cap request

@@ -2987,7 +3311,7 @@ exports[`Confirm should match snapshot for signature - typed sign - permit 1`] = data-testid="confirmation__simulation_section" >
-
+
@@ -3107,7 +3433,7 @@ exports[`Confirm should match snapshot for signature - typed sign - permit 1`] = class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >

Signature request

@@ -3772,7 +4098,7 @@ exports[`Confirm should match snapshot signature - typed sign - order 1`] = ` class="mm-box mm-box--margin-bottom-4 mm-box--padding-2 mm-box--background-color-background-default mm-box--rounded-md" >
{ const mockStatePersonalSign = getMockPersonalSignConfirmState(); const mockStore = configureMockStore(middleware)(mockStatePersonalSign); + let container; await act(async () => { - const { container } = await renderWithConfirmContextProvider( - , - mockStore, - ); - expect(container).toMatchSnapshot(); + const { container: renderContainer } = + await renderWithConfirmContextProvider(, mockStore); + + container = renderContainer; }); + + expect(container).toMatchSnapshot(); }); it('should match snapshot signature - typed sign - order', async () => { @@ -106,8 +107,8 @@ describe('Confirm', () => { }); const mockStore = configureMockStore(middleware)(mockStateTypedSign); - let container; + let container; await act(async () => { const { container: renderContainer } = renderWithConfirmContextProvider( , @@ -123,13 +124,15 @@ describe('Confirm', () => { const mockStateTypedSign = getMockTypedSignConfirmState(); const mockStore = configureMockStore(middleware)(mockStateTypedSign); + let container; await act(async () => { - const { container } = await renderWithConfirmContextProvider( - , - mockStore, - ); - expect(container).toMatchSnapshot(); + const { container: renderContainer } = + await renderWithConfirmContextProvider(, mockStore); + + container = renderContainer; }); + + expect(container).toMatchSnapshot(); }); it('should match snapshot for signature - typed sign - V4 - PermitSingle', async () => { From 4c3232c8c5626b94d59b9e3cf20352dfb9e0a7d1 Mon Sep 17 00:00:00 2001 From: Jony Bursztyn Date: Mon, 7 Oct 2024 15:09:41 +0100 Subject: [PATCH 04/12] feat: change survey timeout time from a week to a day (#27603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Changes the survey time so that users check on a new survey every day instead of every week [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27603?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- ui/components/ui/survey-toast/survey-toast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/ui/survey-toast/survey-toast.tsx b/ui/components/ui/survey-toast/survey-toast.tsx index ff485b2b73e3..7359b9ecd480 100644 --- a/ui/components/ui/survey-toast/survey-toast.tsx +++ b/ui/components/ui/survey-toast/survey-toast.tsx @@ -59,7 +59,7 @@ export function SurveyToast() { signal: controller.signal, }, functionName: 'fetchSurveys', - cacheOptions: { cacheRefreshTime: process.env.IN_TEST ? 0 : DAY * 7 }, + cacheOptions: { cacheRefreshTime: process.env.IN_TEST ? 0 : DAY }, }); const _survey: Survey = response?.surveys; From 60ae8cbc4fd72a64745a6c19e428e91b16a83df3 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 7 Oct 2024 15:36:21 +0100 Subject: [PATCH 05/12] feat: support Etherscan API keys (#27611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Upgrade the `TransactionController` to: - Support Etherscan API keys when polling for incoming transactions. - Populate `submitHistory` to aid with debug and persist even when resetting the account. Also adds the `ETHERSCAN_API_KEY` environment variable. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27611?quickstart=1) ## **Related issues** ## **Manual testing steps** 1. Verify incoming transactions work on Mainnet and Sepolia with an API key set. 2. Verify `submitHistory` is populated in state logs after creating transactions and retrying, on both Infura and custom networks. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .metamaskrc.dist | 4 ++++ app/scripts/metamask-controller.js | 4 ++++ builds.yml | 5 ++++- package.json | 2 +- .../errors-after-init-opt-in-background-state.json | 1 + .../errors-after-init-opt-in-ui-state.json | 1 + ui/index.js | 3 --- yarn.lock | 10 +++++----- 8 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.metamaskrc.dist b/.metamaskrc.dist index 601105e2af44..fc2a5a831a4b 100644 --- a/.metamaskrc.dist +++ b/.metamaskrc.dist @@ -45,3 +45,7 @@ BLOCKAID_PUBLIC_KEY= ; Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render ; This should NEVER be enabled in production since it slows down react ; ENABLE_WHY_DID_YOU_RENDER=false + +; API key used in Etherscan requests to prevent rate limiting. +; Only applies to Mainnet and Sepolia. +; ETHERSCAN_API_KEY= diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0e4a38bbf7a5..31dc5fef3fcd 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1854,6 +1854,10 @@ export default class MetamaskController extends EventEmitter { getCurrentChainId({ metamask: this.networkController.state }) ], incomingTransactions: { + etherscanApiKeysByChainId: { + [CHAIN_IDS.MAINNET]: process.env.ETHERSCAN_API_KEY, + [CHAIN_IDS.SEPOLIA]: process.env.ETHERSCAN_API_KEY, + }, includeTokenTransfers: false, isEnabled: () => Boolean( diff --git a/builds.yml b/builds.yml index 225662347689..a69bf611a322 100644 --- a/builds.yml +++ b/builds.yml @@ -273,8 +273,10 @@ env: - SECURITY_ALERTS_API_ENABLED: '' # URL of security alerts API used to validate dApp requests - SECURITY_ALERTS_API_URL: 'http://localhost:3000' + # API key to authenticate Etherscan requests to avoid rate limiting + - ETHERSCAN_API_KEY: '' - # Enables the notifications feature within the build: + # Enables the notifications feature within the build: - NOTIFICATIONS: '' - METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io @@ -291,6 +293,7 @@ env: ### - EIP_4337_ENTRYPOINT: null + ### # Enable/disable why did you render debug tool: https://github.com/welldone-software/why-did-you-render # This should NEVER be enabled in production since it slows down react diff --git a/package.json b/package.json index 657c8110f09a..e35d2a5f6c36 100644 --- a/package.json +++ b/package.json @@ -359,7 +359,7 @@ "@metamask/snaps-rpc-methods": "^11.1.1", "@metamask/snaps-sdk": "^6.5.1", "@metamask/snaps-utils": "^8.1.1", - "@metamask/transaction-controller": "^37.1.0", + "@metamask/transaction-controller": "^37.2.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^9.1.0", "@ngraveio/bc-ur": "^1.1.12", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index e3efb4a9a728..8d8c8c1ae895 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -304,6 +304,7 @@ }, "TxController": { "methodData": "object", + "submitHistory": "object", "transactions": "object", "lastFetchedBlockNumbers": "object", "submitHistory": "object" diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index af8b816456fe..b1131ec4e7a2 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -198,6 +198,7 @@ "isSignedIn": "boolean", "isProfileSyncingEnabled": null, "isProfileSyncingUpdateLoading": "boolean", + "submitHistory": "object", "subscriptionAccountsSeen": "object", "isMetamaskNotificationsFeatureSeen": "boolean", "isNotificationServicesEnabled": "boolean", diff --git a/ui/index.js b/ui/index.js index fec0321164dd..5cb576e488d6 100644 --- a/ui/index.js +++ b/ui/index.js @@ -290,9 +290,6 @@ function setupStateHooks(store) { // for more info) state.version = global.platform.getVersion(); state.browser = window.navigator.userAgent; - state.completeTxList = await actions.getTransactions({ - filterToCurrentNetwork: false, - }); return state; }; window.stateHooks.getSentryAppState = function () { diff --git a/yarn.lock b/yarn.lock index 2a0ea87667a1..018b0e30269c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6462,9 +6462,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^37.1.0": - version: 37.1.0 - resolution: "@metamask/transaction-controller@npm:37.1.0" +"@metamask/transaction-controller@npm:^37.2.0": + version: 37.2.0 + resolution: "@metamask/transaction-controller@npm:37.2.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6491,7 +6491,7 @@ __metadata: "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^20.0.0 "@metamask/network-controller": ^21.0.0 - checksum: 10/b265c73f3410660ca2021f091508d6dec5e6b9cce240e420f91dd87f48d846ccca68d23892860fabd32dba62551476a9a7476d240679d58a50c1eab4cd04ab82 + checksum: 10/0850797efb2157de41eaec153d31f8f63d194d2290fa41a3d439a28f95a35436f47d56546b0fa64427294280476d11ab4a7ed6161a13ad6f8215a3bc052a41e2 languageName: node linkType: hard @@ -26103,7 +26103,7 @@ __metadata: "@metamask/snaps-utils": "npm:^8.1.1" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:^8.4.0" - "@metamask/transaction-controller": "npm:^37.1.0" + "@metamask/transaction-controller": "npm:^37.2.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^9.1.0" "@ngraveio/bc-ur": "npm:^1.1.12" From 93500d2c2586b2a2867d4002ab4eb46658262f30 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 7 Oct 2024 17:04:52 +0200 Subject: [PATCH 06/12] fix(btc): do not show percentage for tokens (#27637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We were wrongly displaying some percentage (with Ethereum token values) for Bitcoin, but we don't have any data for Bitcoin right now, so we just get rid of those for now. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27637?quickstart=1) ## **Related issues** N/A ## **Manual testing steps** 1. `yarn start:flask` 2. Make sure to select Ethereum mainnet (we do not display any percentage for testnets) 3. Enable Bitcoin support: Settings > Experimental > "Enable Bitcoin support" 4. Create a Bitcoin account 5. You should not see any percentage on the token tab ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-04 at 18 28 32](https://github.com/user-attachments/assets/e939cfb5-54bf-43e2-bf8e-49c5363c6e05) ![Screenshot 2024-10-04 at 18 28 38](https://github.com/user-attachments/assets/91d44bb8-8eb6-475c-885e-b773af0f76b6) ### **After** ![Screenshot 2024-10-04 at 18 27 26](https://github.com/user-attachments/assets/41495464-98f3-400e-b15e-bf412b26ff11) ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../app/assets/token-cell/token-cell.test.tsx | 8 ++++- .../token-list-item/token-list-item.tsx | 30 +++++++++++-------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/ui/components/app/assets/token-cell/token-cell.test.tsx b/ui/components/app/assets/token-cell/token-cell.test.tsx index 749fc50fd98e..882c80964d5b 100644 --- a/ui/components/app/assets/token-cell/token-cell.test.tsx +++ b/ui/components/app/assets/token-cell/token-cell.test.tsx @@ -6,7 +6,10 @@ import { useSelector } from 'react-redux'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; import { getTokenList } from '../../../../selectors'; -import { getMultichainCurrentChainId } from '../../../../selectors/multichain'; +import { + getMultichainCurrentChainId, + getMultichainIsEvm, +} from '../../../../selectors/multichain'; import { useIsOriginalTokenSymbol } from '../../../../hooks/useIsOriginalTokenSymbol'; import { getIntlLocale } from '../../../../ducks/locale/locale'; @@ -101,6 +104,9 @@ describe('Token Cell', () => { if (selector === getMultichainCurrentChainId) { return '0x89'; } + if (selector === getMultichainIsEvm) { + return true; + } if (selector === getIntlLocale) { return 'en-US'; } diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx index a653a803dc1c..a5d6cf385e36 100644 --- a/ui/components/multichain/token-list-item/token-list-item.tsx +++ b/ui/components/multichain/token-list-item/token-list-item.tsx @@ -116,9 +116,13 @@ export const TokenListItem = ({ return undefined; }); + // We do not want to display any percentage with non-EVM since we don't have the data for this yet. So + // we only use this option for EVM here: + const shouldShowPercentage = isEvm && showPercentage; + // Scam warning const showScamWarning = - isNativeCurrency && !isOriginalTokenSymbol && showPercentage; + isNativeCurrency && !isOriginalTokenSymbol && shouldShowPercentage; const dispatch = useDispatch(); const [showScamWarningModal, setShowScamWarningModal] = useState(false); @@ -146,7 +150,9 @@ export const TokenListItem = ({ : null; const tokenTitle = getTokenTitle(); - const tokenMainTitleToDisplay = showPercentage ? tokenTitle : tokenSymbol; + const tokenMainTitleToDisplay = shouldShowPercentage + ? tokenTitle + : tokenSymbol; const stakeableTitle = ( )} - {isEvm && !showPercentage ? ( - - {tokenTitle} - - ) : ( + {shouldShowPercentage ? ( + ) : ( + + {tokenTitle} + )} From 8dedd3c4df8b7b83c2cde53c518a6d5a3f15dfdb Mon Sep 17 00:00:00 2001 From: Derek Brans Date: Mon, 7 Oct 2024 12:03:16 -0400 Subject: [PATCH 07/12] build: add lottie-web dependency to extension (#27632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds a new dependency, [Lottie](https://airbnb.io/lottie), to the extension. Lottie is an animation library. The pros: * Enhanced User Experience: Lottie enables us to add high-quality, lightweight animations that will make our extension more visually appealing. * Community Support: It is well-maintained by Airbnb and has a strong community behind it. * Consistency with Mobile App: We’re already using Lottie in mobile, so integrating it into the extension will provide a consistent user experience across platforms. With any additional dependency, we need a consider: * supply chain attack surface managed by lavamoat. * increase in bundle size – in this case < 100kB gzip-ed. [More context on slack](https://consensys.slack.com/archives/CTQAGKY5V/p1717772021376029). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27632?quickstart=1) ## **Related issues** * **Related:** https://github.com/MetaMask/metamask-extension/pull/27650 – use lottie animation in the extension. ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- jest.config.js | 6 +- package.json | 1 + test/jest/setup.js | 8 ++ .../lottie-animation/index.ts | 2 + .../lottie-animation.test.tsx | 133 ++++++++++++++++++ .../lottie-animation/lottie-animation.tsx | 75 ++++++++++ yarn.lock | 8 ++ 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 ui/components/component-library/lottie-animation/index.ts create mode 100644 ui/components/component-library/lottie-animation/lottie-animation.test.tsx create mode 100644 ui/components/component-library/lottie-animation/lottie-animation.tsx diff --git a/jest.config.js b/jest.config.js index dbfb0522cff7..f1d38ab4aea3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -22,7 +22,11 @@ module.exports = { // TODO: enable resetMocks // resetMocks: true, restoreMocks: true, - setupFiles: ['/test/setup.js', '/test/env.js'], + setupFiles: [ + 'jest-canvas-mock', + '/test/setup.js', + '/test/env.js', + ], setupFilesAfterEnv: ['/test/jest/setup.js'], testMatch: [ '/app/scripts/**/*.test.(js|ts|tsx)', diff --git a/package.json b/package.json index e35d2a5f6c36..cc317effc2ae 100644 --- a/package.json +++ b/package.json @@ -408,6 +408,7 @@ "localforage": "^1.9.0", "lodash": "^4.17.21", "loglevel": "^1.8.1", + "lottie-web": "^5.12.2", "luxon": "^3.2.1", "nanoid": "^2.1.6", "pify": "^5.0.0", diff --git a/test/jest/setup.js b/test/jest/setup.js index 0ee19a4d61b8..77fbb92783bc 100644 --- a/test/jest/setup.js +++ b/test/jest/setup.js @@ -1,6 +1,14 @@ // This file is for Jest-specific setup only and runs before our Jest tests. import '../helpers/setup-after-helper'; +jest.mock('webextension-polyfill', () => { + return { + runtime: { + getManifest: () => ({ manifest_version: 2 }), + }, + }; +}); + jest.mock('../../ui/hooks/usePetnamesEnabled', () => ({ usePetnamesEnabled: () => false, })); diff --git a/ui/components/component-library/lottie-animation/index.ts b/ui/components/component-library/lottie-animation/index.ts new file mode 100644 index 000000000000..dd89a159f751 --- /dev/null +++ b/ui/components/component-library/lottie-animation/index.ts @@ -0,0 +1,2 @@ +export { LottieAnimation } from './lottie-animation'; +export type { LottieAnimationProps } from './lottie-animation'; diff --git a/ui/components/component-library/lottie-animation/lottie-animation.test.tsx b/ui/components/component-library/lottie-animation/lottie-animation.test.tsx new file mode 100644 index 000000000000..21168d788f2c --- /dev/null +++ b/ui/components/component-library/lottie-animation/lottie-animation.test.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import lottie from 'lottie-web/build/player/lottie_light'; +import { LottieAnimation } from './lottie-animation'; + +// Mock lottie-web +jest.mock('lottie-web/build/player/lottie_light', () => { + const eventListeners: { [key: string]: (() => void) | undefined } = {}; + return { + loadAnimation: jest.fn(() => ({ + destroy: jest.fn(), + addEventListener: jest.fn((event: string, callback: () => void) => { + eventListeners[event] = callback; + }), + removeEventListener: jest.fn((event: string) => { + delete eventListeners[event]; + }), + // Method to trigger the 'complete' event in tests + triggerComplete: () => eventListeners.complete?.(), + })), + }; +}); + +describe('LottieAnimation', () => { + const mockData = { + /* Your mock animation data here */ + }; + const mockPath = 'https://example.com/animation.json'; + + it('renders without crashing', () => { + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('applies custom className', () => { + const customClass = 'custom-class'; + const { container } = render( + , + ); + expect(container.firstChild).toHaveClass(customClass); + }); + + it('applies custom style', () => { + const customStyle = { width: '100px', height: '100px' }; + const { container } = render( + , + ); + const element = container.firstChild as HTMLElement; + expect(element).toHaveStyle('width: 100px'); + expect(element).toHaveStyle('height: 100px'); + }); + + it('calls lottie.loadAnimation with correct config when using data', () => { + render(); + + expect(lottie.loadAnimation).toHaveBeenCalledWith( + expect.objectContaining({ + animationData: mockData, + loop: false, + autoplay: false, + renderer: 'svg', + container: expect.any(HTMLElement), + }), + ); + }); + + it('calls lottie.loadAnimation with correct config when using path', () => { + render(); + + expect(lottie.loadAnimation).toHaveBeenCalledWith( + expect.objectContaining({ + path: mockPath, + loop: true, + autoplay: true, + renderer: 'svg', + container: expect.any(HTMLElement), + }), + ); + }); + + it('logs an error when neither data nor path is provided', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + render(); + + expect(consoleSpy).toHaveBeenCalledWith( + 'LottieAnimation: Exactly one of data or path must be provided', + ); + consoleSpy.mockRestore(); + }); + + it('logs an error when both data and path are provided', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + render(); + + expect(consoleSpy).toHaveBeenCalledWith( + 'LottieAnimation: Exactly one of data or path must be provided', + ); + consoleSpy.mockRestore(); + }); + + it('calls onComplete when animation completes', () => { + const onCompleteMock = jest.fn(); + + render(); + + const animationInstance = (lottie.loadAnimation as jest.Mock).mock + .results[0].value; + + act(() => { + animationInstance.triggerComplete(); + }); + + expect(onCompleteMock).toHaveBeenCalledTimes(1); + }); + + it('removes event listener on unmount', () => { + const onCompleteMock = jest.fn(); + + const { unmount } = render( + , + ); + + const animationInstance = (lottie.loadAnimation as jest.Mock).mock + .results[0].value; + + unmount(); + + expect(animationInstance.removeEventListener).toHaveBeenCalledWith( + 'complete', + expect.any(Function), + ); + }); +}); diff --git a/ui/components/component-library/lottie-animation/lottie-animation.tsx b/ui/components/component-library/lottie-animation/lottie-animation.tsx new file mode 100644 index 000000000000..08f9c783c7f3 --- /dev/null +++ b/ui/components/component-library/lottie-animation/lottie-animation.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useRef } from 'react'; +import { + AnimationConfigWithData, + AnimationConfigWithPath, + AnimationItem, +} from 'lottie-web'; +// Use lottie_light to avoid unsafe-eval which breaks the CSP +// https://github.com/airbnb/lottie-web/issues/289#issuecomment-1454909624 +import lottie from 'lottie-web/build/player/lottie_light'; + +export type LottieAnimationProps = { + data?: object; + path?: string; + loop?: boolean; + autoplay?: boolean; + style?: React.CSSProperties; + className?: string; + onComplete?: () => void; +}; + +export const LottieAnimation: React.FC = ({ + data, + path, + loop = true, + autoplay = true, + style = {}, + className = '', + onComplete = () => null, +}) => { + const containerRef = useRef(null); + const animationInstance = useRef(null); + + useEffect(() => { + if (!containerRef.current) { + console.error('LottieAnimation: containerRef is null'); + return () => null; + } + + if (Boolean(data) === Boolean(path)) { + console.error( + 'LottieAnimation: Exactly one of data or path must be provided', + ); + return () => null; + } + + const animationConfig: AnimationConfigWithData | AnimationConfigWithPath = { + container: containerRef.current, + renderer: 'svg', + loop, + autoplay, + ...(data ? { animationData: data } : { path }), + }; + + try { + animationInstance.current = lottie.loadAnimation(animationConfig); + animationInstance.current.addEventListener('complete', onComplete); + + animationInstance.current.addEventListener('error', (error) => { + console.error('LottieAnimation error:', error); + }); + } catch (error) { + console.error('Failed to load animation:', error); + } + + return () => { + if (animationInstance.current) { + animationInstance.current.removeEventListener('complete', onComplete); + animationInstance.current.destroy(); + animationInstance.current = null; + } + }; + }, [data, path, loop, autoplay, onComplete]); + + return
; +}; diff --git a/yarn.lock b/yarn.lock index 018b0e30269c..4cae5223a04c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25302,6 +25302,13 @@ __metadata: languageName: node linkType: hard +"lottie-web@npm:^5.12.2": + version: 5.12.2 + resolution: "lottie-web@npm:5.12.2" + checksum: 10/cd377d54a675b37ac9359306b84097ea402dff3d74a2f45e6e0dbcff1df94b3a978e92e48fd34765754bdbb94bd2d8d4da31954d95f156e77489596b235cac91 + languageName: node + linkType: hard + "lower-case@npm:^2.0.2": version: 2.0.2 resolution: "lower-case@npm:2.0.2" @@ -26295,6 +26302,7 @@ __metadata: lodash: "npm:^4.17.21" loglevel: "npm:^1.8.1" loose-envify: "npm:^1.4.0" + lottie-web: "npm:^5.12.2" luxon: "npm:^3.2.1" mocha: "npm:^10.2.0" mocha-junit-reporter: "npm:^2.2.1" From 43f39891ad689dc4bf13b7aa19c950bf3a654f83 Mon Sep 17 00:00:00 2001 From: Derek Brans Date: Mon, 7 Oct 2024 14:44:42 -0400 Subject: [PATCH 08/12] feat(stx): animations and cosmetic changes to smart transaction status page (#27650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Animations and cosmetic changes to smart transaction status page. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27650?quickstart=1) ## **Related issues** Fixes: * https://consensyssoftware.atlassian.net/browse/TXL-366 * https://consensyssoftware.atlassian.net/browse/TXL-413 * https://consensyssoftware.atlassian.net/browse/TXL-365 * https://consensyssoftware.atlassian.net/browse/TXL-412 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/e05e6333-9300-41ad-9c0e-f55b2f30e2a8 ### After https://github.com/user-attachments/assets/fcaa4b2e-5b49-4c8e-92a0-9721751407c5 ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: MetaMask Bot Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- .prettierignore | 2 + app/_locales/de/messages.json | 11 - app/_locales/el/messages.json | 11 - app/_locales/en/messages.json | 13 +- app/_locales/en_GB/messages.json | 2 +- app/_locales/es/messages.json | 11 - app/_locales/fr/messages.json | 11 - app/_locales/hi/messages.json | 11 - app/_locales/id/messages.json | 11 - app/_locales/ja/messages.json | 11 - app/_locales/ko/messages.json | 11 - app/_locales/pt/messages.json | 11 - app/_locales/ru/messages.json | 11 - app/_locales/tl/messages.json | 11 - app/_locales/tr/messages.json | 11 - app/_locales/vi/messages.json | 11 - app/_locales/zh_CN/messages.json | 11 - .../confirmed.lottie.json | 1 + .../failed.lottie.json | 1 + .../processing.lottie.json | 1 + .../submitting-intro.lottie.json | 1 + .../submitting-loop.lottie.json | 1 + jest.integration.config.js | 1 + lavamoat/browserify/beta/policy.json | 27 + lavamoat/browserify/flask/policy.json | 27 + lavamoat/browserify/main/policy.json | 27 + lavamoat/browserify/mmi/policy.json | 27 + sonar-project.properties | 4 +- .../useSimulationMetrics.ts | 19 +- ...mart-transactions-status-page.test.js.snap | 704 ------------------ ...art-transactions-status-page.test.tsx.snap | 151 ++++ .../smart-transaction-status-page/index.scss | 56 -- ...mart-transaction-status-animation.test.tsx | 134 ++++ .../smart-transaction-status-animation.tsx | 80 ++ .../smart-transaction-status-page.stories.tsx | 97 +++ .../smart-transaction-status-page.tsx | 216 +----- .../smart-transactions-status-page.test.js | 226 ------ .../smart-transactions-status-page.test.tsx | 145 ++++ 38 files changed, 759 insertions(+), 1358 deletions(-) create mode 100644 app/images/animations/smart-transaction-status/confirmed.lottie.json create mode 100644 app/images/animations/smart-transaction-status/failed.lottie.json create mode 100644 app/images/animations/smart-transaction-status/processing.lottie.json create mode 100644 app/images/animations/smart-transaction-status/submitting-intro.lottie.json create mode 100644 app/images/animations/smart-transaction-status/submitting-loop.lottie.json delete mode 100644 ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.js.snap create mode 100644 ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap create mode 100644 ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx create mode 100644 ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx create mode 100644 ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx delete mode 100644 ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js create mode 100644 ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx diff --git a/.prettierignore b/.prettierignore index 9c4b3868464b..d8d8cfe4a15c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,8 @@ *.scss .nyc_output/**/* node_modules/**/* +# Exclude lottie json files +/app/images/animations/**/*.json /app/vendor/** /builds/**/* /coverage/**/* diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 3ff80228ef4f..92cc0f25f1a7 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Ihre Transaktion ist abgeschlossen" }, - "smartTransactionTakingTooLong": { - "message": "Entschuldigung für die Wartezeit" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Wenn Ihre Transaktion nicht innerhalb von $1 abgeschlossen wird, wird sie storniert und Ihnen wird kein Gas berechnet.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Smart Transactions" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Versuchen Sie Ihren Swap erneut. Wir werden hier sein, um Sie beim nächsten Mal vor ähnlichen Risiken zu schützen." }, - "stxEstimatedCompletion": { - "message": "Geschätzter Abschluss in < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap fehlgeschlagen" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index c348e7bb6cbf..c7f7137665a4 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Η συναλλαγή σας ολοκληρώθηκε" }, - "smartTransactionTakingTooLong": { - "message": "Συγγνώμη για την αναμονή" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Εάν η συναλλαγή σας δεν ολοκληρωθεί εντός $1, θα ακυρωθεί και δεν θα χρεωθείτε με τέλη συναλλαγών.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Έξυπνες συναλλαγές" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Προσπαθήστε ξανά να κάνετε ανταλλαγή. Θα είμαστε εδώ για να σας προστατεύσουμε από παρόμοιους κινδύνους και την επόμενη φορά." }, - "stxEstimatedCompletion": { - "message": "Εκτιμώμενη ολοκλήρωση σε < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Η ανταλλαγή απέτυχε" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1ddcd1c05a6b..9a94edeb7edf 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5019,18 +5019,11 @@ "message": "Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support." }, "smartTransactionPending": { - "message": "Submitting your transaction" + "message": "Your transaction was submitted" }, "smartTransactionSuccess": { "message": "Your transaction is complete" }, - "smartTransactionTakingTooLong": { - "message": "Sorry for the wait" - }, - "smartTransactionTakingTooLongDescription": { - "message": "If your transaction is not finalized within $1, it will be canceled and you will not be charged for gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Smart Transactions" }, @@ -5456,10 +5449,6 @@ "stxCancelledSubDescription": { "message": "Try your swap again. We’ll be here to protect you against similar risks next time." }, - "stxEstimatedCompletion": { - "message": "Estimated completion in < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap failed" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 71599915880e..d02d9b8c1af5 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -4821,7 +4821,7 @@ "message": "Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support." }, "smartTransactionPending": { - "message": "Submitting your transaction" + "message": "Your transaction was submitted" }, "smartTransactionSuccess": { "message": "Your transaction is complete" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 05015a3de622..3430b44cad96 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -4731,13 +4731,6 @@ "smartTransactionSuccess": { "message": "Su transacción está completa" }, - "smartTransactionTakingTooLong": { - "message": "Disculpe la espera" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Si su transacción no finaliza en $1, se cancelará y no se le cobrará el gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transacciones inteligentes" }, @@ -5154,10 +5147,6 @@ "stxCancelledSubDescription": { "message": "Intente su swap nuevamente. Estaremos aquí para protegerlo contra riesgos similares la próxima vez." }, - "stxEstimatedCompletion": { - "message": "Finalización estimada en < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Error al intercambiar" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 8301ad348b07..b2429962bad3 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Votre transaction est terminée" }, - "smartTransactionTakingTooLong": { - "message": "Désolé de vous avoir fait attendre" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Si votre transaction n’est pas finalisée dans un délai de $1, elle sera annulée et les frais de gaz ne vous seront pas facturés.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transactions intelligentes" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Réessayez le swap. Nous serons là pour vous protéger contre des risques similaires la prochaine fois." }, - "stxEstimatedCompletion": { - "message": "Délai estimé < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Échec du swap" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 0a5423979efa..91bcfebef973 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "आपका ट्रांसेक्शन पूरा हो गया है" }, - "smartTransactionTakingTooLong": { - "message": "माफ़ी चाहते हैं कि आपको इंतज़ार करना पड़ा" - }, - "smartTransactionTakingTooLongDescription": { - "message": "यदि आपका ट्रांसेक्शन $1 के भीतर फाइनलाइज़ नहीं होता है, तो इसे कैंसिल कर दिया जाएगा और आपसे गैस फ़ीस नहीं ली जाएगी।", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "स्मार्ट ट्रांसेक्शन" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "अपना स्वैप फिर से कोशिश करें। अगली बार भी इस तरह के जोखिमों से आपको बचाने के लिए हम यहां होंगे।" }, - "stxEstimatedCompletion": { - "message": "<$1 में पूरा होने का अनुमान", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "स्वैप नहीं हो पाया" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 054150ae5b7a..82ab45bdfa99 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Transaksi Anda selesai" }, - "smartTransactionTakingTooLong": { - "message": "Maaf telah menunggu" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Jika transaksi tidak diselesaikan dalam $1, transaksi akan dibatalkan dan Anda tidak akan dikenakan biaya gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transaksi Pintar" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Cobalah untuk menukar lagi. Kami akan selalu hadir untuk melindungi Anda dari risiko serupa di lain waktu." }, - "stxEstimatedCompletion": { - "message": "Estimasi penyelesaian dalam < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Pertukaran gagal" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 74f281b7f873..280889881f57 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "トランザクションが完了しました" }, - "smartTransactionTakingTooLong": { - "message": "お待たせして申し訳ございません" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1以内にトランザクションが完了しない場合はキャンセルされ、ガス代は請求されません。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "スマートトランザクション" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "もう一度スワップをお試しください。次回は同様のリスクを避けられるようサポートします。" }, - "stxEstimatedCompletion": { - "message": "$1未満で完了予定", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "スワップに失敗しました" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index ce11ea4bebf4..c1591b2fc28e 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "트랜잭션 완료" }, - "smartTransactionTakingTooLong": { - "message": "기다리게 해서 죄송합니다" - }, - "smartTransactionTakingTooLongDescription": { - "message": "$1 이내에 트랜잭션이 완료되지 않으면 트랜잭션이 취소되고 가스비가 부과되지 않습니다.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "스마트 트랜잭션" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "스왑을 다시 진행하세요. 다음에도 유사한 위험이 발생한다면 보호해 드리겠습니다." }, - "stxEstimatedCompletion": { - "message": "예상 잔여 시간: <$1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "스왑 실패" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 770d517c8b25..95637cb057f9 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Sua transação foi concluída" }, - "smartTransactionTakingTooLong": { - "message": "Desculpe pela espera" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Se a sua transação não for finalizada em $1, ela será cancelada e você não pagará gás.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Transações inteligentes" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Tente trocar novamente. Estaremos aqui para proteger você contra riscos semelhantes no futuro." }, - "stxEstimatedCompletion": { - "message": "Conclusão estimada em até $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Falha na troca" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 6ce21329f244..6ce19f83b4ed 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Ваша транзакция завершена" }, - "smartTransactionTakingTooLong": { - "message": "Извините за ожидание" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Если ваша транзакция не будет завершена в течение $1, она будет отменена и с вас не будет взиматься плата за газ.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Умные транзакции" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Попробуйте выполнить своп еще раз. Мы готовы защитить вас от подобных рисков в следующий раз." }, - "stxEstimatedCompletion": { - "message": "Предполагаемое завершение через < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Своп не удался" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 57b62731724d..1246c2a085a1 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Nakumpleto ang transaksyon mo" }, - "smartTransactionTakingTooLong": { - "message": "Paumanhin sa paghihintay" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Kung ang transaksyon mo ay hindi natapos sa loob ng $1, ito ay kakanselahin at hindi ka sisingilin para sa gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Mga Smart Transaction" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Subukan muli ang pag-swap. Narito kami para protektahan ka sa mga katulad na panganib sa susunod." }, - "stxEstimatedCompletion": { - "message": "Tinatayang pagkumpleto sa loob ng < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Nabigo ang pag-swap" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 9f8e90a386e3..eedc60659269 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "İşleminiz tamamlandı" }, - "smartTransactionTakingTooLong": { - "message": "Beklettiğimiz için özür dileriz" - }, - "smartTransactionTakingTooLongDescription": { - "message": "İşleminiz $1 dahilinde sonuçlanmazsa iptal edilir ve sizden gaz ücreti alınmaz.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Akıllı İşlemler" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Swap işlemini tekrar deneyin. Bir dahaki sefere sizi benzer risklere karşı korumak için burada olacağız." }, - "stxEstimatedCompletion": { - "message": "Tamamlanmasına kalan tahmini süre $1 altında", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Swap başarısız oldu" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 273089bb4343..955c302f19a8 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "Giao dịch của bạn đã hoàn tất" }, - "smartTransactionTakingTooLong": { - "message": "Xin lỗi đã để bạn đợi lâu" - }, - "smartTransactionTakingTooLongDescription": { - "message": "Nếu giao dịch của bạn không được hoàn thành trong vòng $1, thì giao dịch sẽ bị hủy và bạn sẽ không bị tính phí gas.", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "Giao dịch thông minh" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "Hãy thử hoán đổi lại. Chúng tôi ở đây để bảo vệ bạn trước những rủi ro tương tự trong lần tới." }, - "stxEstimatedCompletion": { - "message": "Dự kiến hoàn thành sau < $1", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "Hoán đổi không thành công" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 6a8f8d9f4df6..14395afca8b8 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -4734,13 +4734,6 @@ "smartTransactionSuccess": { "message": "您的交易已完成" }, - "smartTransactionTakingTooLong": { - "message": "抱歉让您久等" - }, - "smartTransactionTakingTooLongDescription": { - "message": "如果您的交易在 $1 内未完成,则会取消,您无需支付燃料费。", - "description": "$1 is remaining time in seconds" - }, "smartTransactions": { "message": "智能交易" }, @@ -5157,10 +5150,6 @@ "stxCancelledSubDescription": { "message": "再次尝试进行交换。下次我们会在这里保护您免受类似风险。 " }, - "stxEstimatedCompletion": { - "message": "预计将在 $1 内完成", - "description": "$1 is remeaning time in minutes and seconds, e.g. 0:10" - }, "stxFailure": { "message": "交换失败" }, diff --git a/app/images/animations/smart-transaction-status/confirmed.lottie.json b/app/images/animations/smart-transaction-status/confirmed.lottie.json new file mode 100644 index 000000000000..d5552d380b45 --- /dev/null +++ b/app/images/animations/smart-transaction-status/confirmed.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":175,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Confirmation 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-79,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-69,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-26,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.909,0.909,0.333],"y":[0,0,0]},"t":0,"s":[80,80,100]},{"i":{"x":[1,1,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":15,"s":[92,92,100]},{"t":36,"s":[20,20,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-53,"s":[0]},{"t":-24,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":-108,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Confirmation","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[11.185,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-58.942,-0.221],[-19.5,39.221],[58.942,-39.221]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.725],"y":[0]},"t":81,"s":[0]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.566],"y":[0]},"t":89,"s":[26.7]},{"t":102,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":81,"op":175,"st":81,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Stroke Contour","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[96,96,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":19.207,"s":[0]},{"t":76,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":14,"s":[0]},{"t":58.79296875,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":175,"st":14,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.356,0.356,0.667],"y":[1,1,1]},"o":{"x":[0.015,0.015,0.333],"y":[1.1,1.1,0]},"t":49,"s":[30,30,100]},{"i":{"x":[0,0,0.833],"y":[1,1,1]},"o":{"x":[0.694,0.694,0.167],"y":[0,0,0]},"t":89,"s":[100,100,100]},{"t":135,"s":[89,89,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":49,"op":175,"st":49,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Xplosion 6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[428,428,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Xplosion 8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[364,376,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Xplosion 12","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[190,22,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Xplosion 11","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[351,462,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Xplosion 10","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[54,460,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Xplosion 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[427,66,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Xplosion 4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[28,247,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[71,71,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Xplosion 9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[243,409,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Xplosion 7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[250,92,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Xplosion 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[477,245,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Xplosion 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[72,373,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Xplosion","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":119,"s":[100]},{"t":147,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":106,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[84,90,0]}],"ix":2},"a":{"a":0,"k":[-95.268,-157.268,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.465,21.465],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235353956,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-95.268,-157.268],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[162.989,162.989],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":81,"op":175,"st":14,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.356,0.356,0.667],"y":[1,1,1]},"o":{"x":[0.015,0.015,0.333],"y":[1.1,1.1,0]},"t":37,"s":[30,30,100]},{"t":77,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.722089460784,0.722089460784,0.722089460784,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":37,"op":89,"st":37,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/failed.lottie.json b/app/images/animations/smart-transaction-status/failed.lottie.json new file mode 100644 index 000000000000..f2405f22c72d --- /dev/null +++ b/app/images/animations/smart-transaction-status/failed.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":175,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Fail","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.086,0.086,0.667],"y":[0.96,0.96,1]},"o":{"x":[0.015,0.015,0.333],"y":[0.599,0.599,0]},"t":41,"s":[40,40,100]},{"i":{"x":[0,0,0.833],"y":[1,1,1]},"o":{"x":[0.539,0.539,0.167],"y":[-0.194,-0.194,0]},"t":71,"s":[80,80,100]},{"t":103,"s":[75,75,100]}],"ix":6}},"ao":0,"ip":41,"op":179,"st":4,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"exlamation point","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,59.504,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0],[0,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"exclamation line","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,-0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-26.571],[0,26.571]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"triangle","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.014,-0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.306,-7.458],[0,0],[-8.611,0],[0,0],[4.306,7.458],[0,0]],"o":[[0,0],[-4.306,7.458],[0,0],[8.611,0],[0,0],[-4.306,-7.458]],"v":[[-9.688,-95.736],[-113.775,84.549],[-104.088,101.329],[104.088,101.329],[113.775,84.549],[9.688,-95.736]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.878431372549,0.392156862745,0.439215686275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":41,"op":180,"st":5,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-79,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-69,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-26,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.909,0.909,0.333],"y":[0,0,0]},"t":0,"s":[80,80,100]},{"i":{"x":[1,1,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":15,"s":[92,92,100]},{"t":36,"s":[20,20,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-53,"s":[0]},{"t":-24,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":36,"st":-108,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Stroke Contour","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[96,96,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-76.193],[76.193,0],[0,76.193],[-76.193,0]],"o":[[0,76.193],[-76.193,0],[0,-76.193],[76.193,0]],"v":[[137.959,0],[0,137.959],[-137.959,0],[0,-137.959]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":19.207,"s":[0]},{"t":76,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.206],"y":[1]},"o":{"x":[0.905],"y":[0]},"t":14,"s":[0]},{"t":58.79296875,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.878431432387,0.392156892664,0.439215716194,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":175,"st":14,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/processing.lottie.json b/app/images/animations/smart-transaction-status/processing.lottie.json new file mode 100644 index 000000000000..96a4356b24ce --- /dev/null +++ b/app/images/animations/smart-transaction-status/processing.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":117,"w":500,"h":500,"nm":"OC_MMSmartTransactions_Processing","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":0,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":9,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":23,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":41,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":70,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":92,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"t":117,"s":[371,284,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":6,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":0,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":4,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":19,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":38,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":60,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":91,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"t":119,"s":[251,234,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":3,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":0,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":17,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":35,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":60,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":90,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"t":117,"s":[130,250,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":117,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/submitting-intro.lottie.json b/app/images/animations/smart-transaction-status/submitting-intro.lottie.json new file mode 100644 index 000000000000..7ab9d476cdaf --- /dev/null +++ b/app/images/animations/smart-transaction-status/submitting-intro.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":112,"w":500,"h":500,"nm":"OC_MMSmartTransactions_SubmittingIntro","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":112,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":39,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":49,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":65,"s":[0]},{"t":94,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":39,"op":112,"st":10,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":-117,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":-100,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":-82,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":-57,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":-27,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"i":{"x":0.223,"y":0.829},"o":{"x":0.167,"y":0},"t":0,"s":[130,250,0],"to":[-1,85,0],"ti":[0,0,0]},{"t":12,"s":[251,238,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":12,"st":-117,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":-117,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":-113,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":-98,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":-79,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":-57,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":-26,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"i":{"x":0.407,"y":0},"o":{"x":0.484,"y":0.816},"t":2,"s":[251,234,0],"to":[-5.375,23.25,0],"ti":[0,0,0]},{"i":{"x":0.258,"y":0.825},"o":{"x":1,"y":0.894},"t":12,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":1,"y":1},"o":{"x":1,"y":0.12},"t":17,"s":[251,214,0],"to":[0,0,0],"ti":[0,0,0]},{"t":39,"s":[167,319,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.118,0.118,0.667],"y":[1,1,1]},"o":{"x":[0.821,0.821,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"t":17,"s":[146,146,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":39,"st":-114,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":-117,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":-108,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":-94,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":-76,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":-47,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":-25,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"i":{"x":0.223,"y":0.822},"o":{"x":0.167,"y":0},"t":0,"s":[371,284,0],"to":[0,0,0],"ti":[65,80.5,0]},{"t":12,"s":[251,235,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":12,"st":-111,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Dash Orb Small 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":85,"s":[40.089]},{"t":92,"s":[-198.911]}],"ix":3},"y":{"a":0,"k":117.5,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":86.5,"s":[0]},{"t":93,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":80,"s":[0]},{"t":86.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":20,"op":112,"st":20,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Dash Orb Small 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":104,"s":[121.104]},{"t":111,"s":[-117.896]}],"ix":3},"y":{"a":0,"k":149.278,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":105.5,"s":[0]},{"t":112,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":99,"s":[0]},{"t":105.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":39,"op":112,"st":39,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Dash Orb Small 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":101,"s":[166.352]},{"t":108,"s":[-72.648]}],"ix":3},"y":{"a":0,"k":-137.592,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":102.5,"s":[0]},{"t":109,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":96,"s":[0]},{"t":102.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":36,"op":112,"st":36,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/images/animations/smart-transaction-status/submitting-loop.lottie.json b/app/images/animations/smart-transaction-status/submitting-loop.lottie.json new file mode 100644 index 000000000000..caf5052ee85f --- /dev/null +++ b/app/images/animations/smart-transaction-status/submitting-loop.lottie.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.7","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":32,"w":500,"h":500,"nm":"OC_MMSmartTransactions_SubmittingLoop","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"CTRL","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":32,"st":-80,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"plane","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.354,"y":0.88},"o":{"x":0.182,"y":1},"t":-41,"s":[189.265,312.957,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.136,"y":1},"o":{"x":0.854,"y":0.176},"t":-31,"s":[168,335,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[250,250,0]}],"ix":2,"x":"var $bm_rt;\n$bm_rt = wiggle($bm_mul(time, 5), effect('Slider Control')('Slider'));"},"a":{"a":0,"k":[2331.957,1839.486,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":-15,"s":[0]},{"t":14,"s":[5]}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.281,-1.65],[0,0],[2.829,3.643],[0,0],[-0.44,2.042],[0,0],[-0.498,-2.312],[0,0]],"o":[[0,0],[-2.829,3.643],[0,0],[-1.281,-1.65],[0,0],[0.498,-2.312],[0,0],[0.44,2.042]],"v":[[2370.826,1917.848],[2337.538,1960.709],[2326.376,1960.709],[2293.088,1917.848],[2291.761,1912.024],[2329.809,1735.544],[2334.104,1735.544],[2372.152,1912.024]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.5,0],[0,0],[0.704,3.319],[0,0],[-1.207,-2.19],[0,0]],"o":[[0,0],[-3.393,0],[0,0],[-0.47,-2.456],[0,0],[2.65,4.81]],"v":[[2443.586,1915.245],[2389.666,1915.245],[2382.638,1909.553],[2336.347,1718.15],[2340.428,1716.677],[2449.917,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.469,-2.456],[0,0],[3.393,0],[0,0],[-2.65,4.81]],"o":[[1.207,-2.19],[0,0],[-0.704,3.319],[0,0],[-5.49,0],[0,0]],"v":[[2323.467,1716.695],[2327.548,1718.168],[2281.274,1909.552],[2274.247,1915.245],[2220.316,1915.245],[2213.997,1904.545]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":-70,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Left Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.039,"y":0.637},"o":{"x":1,"y":0.718},"t":-197,"s":[130,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":0},"o":{"x":1,"y":0.399},"t":-180,"s":[130,180,0],"to":[0,0,0],"ti":[0.031,-37.625,0]},{"i":{"x":0.116,"y":0.616},"o":{"x":0.712,"y":0.351},"t":-162,"s":[130,250,0],"to":[3.938,127.151,0],"ti":[-66.538,75.217,0]},{"i":{"x":0.116,"y":0.173},"o":{"x":0.701,"y":0.488},"t":-137,"s":[367,353,0],"to":[46,-52,0],"ti":[68,26,0]},{"i":{"x":0.223,"y":0.626},"o":{"x":0.769,"y":1},"t":-107,"s":[241,180,0],"to":[-41.772,-15.972,0],"ti":[1,-85,0]},{"i":{"x":0.223,"y":0.829},"o":{"x":0.167,"y":0},"t":-80,"s":[130,250,0],"to":[-1,85,0],"ti":[0,0,0]},{"t":-68,"s":[251,238,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-68.6060606060606,"op":-68,"st":-197,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Center Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.493,"y":0.345},"t":-197,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.904},"o":{"x":0.474,"y":0.527},"t":-193,"s":[250,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.334},"o":{"x":1,"y":0.126},"t":-178,"s":[250,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.116,"y":0.769},"o":{"x":0.56,"y":0.224},"t":-159,"s":[250,250,0],"to":[0,0,0],"ti":[78,-51,0]},{"i":{"x":0.116,"y":0.499},"o":{"x":0.701,"y":0.293},"t":-137,"s":[216,140,0],"to":[-78,51,0],"ti":[-67,85,0]},{"i":{"x":0.667,"y":0.768},"o":{"x":0.803,"y":0.791},"t":-106,"s":[277,328,0],"to":[67,-85,0],"ti":[5.375,-23.25,0]},{"i":{"x":0.407,"y":0},"o":{"x":0.484,"y":0.816},"t":-78,"s":[251,234,0],"to":[-5.375,23.25,0],"ti":[0,0,0]},{"i":{"x":0.258,"y":0.825},"o":{"x":1,"y":0.894},"t":-68,"s":[251,234,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":1,"y":1},"o":{"x":1,"y":0.12},"t":-63,"s":[251,214,0],"to":[0,0,0],"ti":[0,0,0]},{"t":-41,"s":[167,319,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.118,0.118,0.667],"y":[1,1,1]},"o":{"x":[0.821,0.821,0.333],"y":[0,0,0]},"t":-72,"s":[100,100,100]},{"t":-63,"s":[146,146,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-41.6060606060606,"op":-41,"st":-194,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Right Dot","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":1,"y":1},"o":{"x":0.797,"y":0.561},"t":-197,"s":[371,284,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.039,"y":0.701},"o":{"x":0,"y":0},"t":-188,"s":[370,250,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.204,"y":0},"o":{"x":1,"y":0.399},"t":-174,"s":[370,180,0],"to":[0,0,0],"ti":[0.627,-21.241,0]},{"i":{"x":0.059,"y":0.016},"o":{"x":0.391,"y":0.209},"t":-156,"s":[370,250,0],"to":[-2.5,84.75,0],"ti":[15.378,106.092,0]},{"i":{"x":0.116,"y":0.585},"o":{"x":0.756,"y":0.511},"t":-127,"s":[88,232,0],"to":[-18.703,-129.031,0],"ti":[-80,-102,0]},{"i":{"x":0.223,"y":0.67},"o":{"x":0.727,"y":1},"t":-105,"s":[399,141,0],"to":[44.438,56.659,0],"ti":[0,0,0]},{"i":{"x":0.223,"y":0.822},"o":{"x":0.167,"y":0},"t":-80,"s":[371,284,0],"to":[0,0,0],"ti":[65,80.5,0]},{"t":-68,"s":[251,235,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[60,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-68.6060606060606,"op":-68,"st":-191,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Dash Orb Small 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":5,"s":[40.089]},{"t":12,"s":[-198.911]}],"ix":3},"y":{"a":0,"k":117.5,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":6.5,"s":[0]},{"t":13,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":0,"s":[0]},{"t":6.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-60,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Dash Orb Small 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":24,"s":[121.104]},{"t":31,"s":[-117.896]}],"ix":3},"y":{"a":0,"k":149.278,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":25.5,"s":[0]},{"t":32,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":19,"s":[0]},{"t":25.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-41,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Dash Orb Small 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.428],"y":[1]},"o":{"x":[0.578],"y":[0]},"t":21,"s":[166.352]},{"t":28,"s":[-72.648]}],"ix":3},"y":{"a":0,"k":-137.592,"ix":4}},"a":{"a":0,"k":[-49.911,-144,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-27,-144],[-71.5,-144]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.062745098039,0.596078431373,0.988235294118,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":22.5,"s":[0]},{"t":29,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":16,"s":[0]},{"t":22.5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":-44,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/jest.integration.config.js b/jest.integration.config.js index e6635bd5b695..6f5d79484386 100644 --- a/jest.integration.config.js +++ b/jest.integration.config.js @@ -18,6 +18,7 @@ module.exports = { ], restoreMocks: true, setupFiles: [ + 'jest-canvas-mock', '/test/integration/config/setup.js', '/test/integration/config/env.js', ], diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index a2d6685880fb..c8c97ce1dd8a 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -4608,6 +4608,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index a2d6685880fb..c8c97ce1dd8a 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -4608,6 +4608,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index a2d6685880fb..c8c97ce1dd8a 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -4608,6 +4608,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 28828b13e737..7478c04ea3aa 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -4700,6 +4700,33 @@ "navigator": true } }, + "lottie-web": { + "globals": { + "Blob": true, + "Howl": true, + "OffscreenCanvas": true, + "URL.createObjectURL": true, + "Worker": true, + "XMLHttpRequest": true, + "bodymovin": "write", + "clearInterval": true, + "console": true, + "define": true, + "document.body": true, + "document.createElement": true, + "document.createElementNS": true, + "document.getElementsByClassName": true, + "document.getElementsByTagName": true, + "document.querySelectorAll": true, + "document.readyState": true, + "location.origin": true, + "location.pathname": true, + "navigator": true, + "requestAnimationFrame": true, + "setInterval": true, + "setTimeout": true + } + }, "luxon": { "globals": { "Intl": true diff --git a/sonar-project.properties b/sonar-project.properties index a965dab30d7e..ad18a60d6fc7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,9 @@ sonar.organization=consensys # Source sonar.sources=app,development,offscreen,shared,types,ui -sonar.exclusions=**/*.test.**,**/*.spec.**,app/images,test/e2e/page-objects,test/data + +# Exclude tests and stories from all analysis (to avoid code coverage, duplicate code, security issues, etc.) +sonar.exclusions=**/*.test.**,**/*.spec.**,app/images,test/e2e/page-objects,test/data,**/*.stories.js,**/*.stories.tsx # Tests sonar.tests=app,development,offscreen,shared,test,types,ui diff --git a/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts b/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts index 178561d0f5d6..b64a83394898 100644 --- a/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts +++ b/ui/pages/confirmations/components/simulation-details/useSimulationMetrics.ts @@ -67,13 +67,14 @@ export function useSimulationMetrics({ setLoadingComplete(); } - const displayNameRequests: UseDisplayNameRequest[] = balanceChanges.map( - ({ asset }) => ({ - value: asset.address ?? '', + const displayNameRequests: UseDisplayNameRequest[] = balanceChanges + // Filter out changes with no address (e.g. ETH) + .filter(({ asset }) => Boolean(asset.address)) + .map(({ asset }) => ({ + value: asset.address as string, type: NameType.ETHEREUM_ADDRESS, preferContractSymbol: true, - }), - ); + })); const displayNames = useDisplayNames(displayNameRequests); @@ -145,7 +146,9 @@ export function useSimulationMetrics({ function useIncompleteAssetEvent( balanceChanges: BalanceChange[], - displayNamesByAddress: { [address: string]: UseDisplayNameResponse }, + displayNamesByAddress: { + [address: string]: UseDisplayNameResponse | undefined; + }, ) { const trackEvent = useContext(MetaMetricsContext); const [processedAssets, setProcessedAssets] = useState([]); @@ -170,7 +173,7 @@ function useIncompleteAssetEvent( properties: { asset_address: change.asset.address, asset_petname: getPetnameType(change, displayName), - asset_symbol: displayName.contractDisplayName, + asset_symbol: displayName?.contractDisplayName, asset_type: getAssetType(change.asset.standard), fiat_conversion_available: change.fiatAmount ? FiatType.Available @@ -244,7 +247,7 @@ function getAssetType(standard: TokenStandard) { function getPetnameType( balanceChange: BalanceChange, - displayName: UseDisplayNameResponse, + displayName: UseDisplayNameResponse = { name: '', hasPetname: false }, ) { if (balanceChange.asset.standard === TokenStandard.none) { return PetnameType.Default; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.js.snap b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.js.snap deleted file mode 100644 index bc80da580732..000000000000 --- a/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.js.snap +++ /dev/null @@ -1,704 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SmartTransactionStatusPage renders the "Sorry for the wait" pending status 1`] = ` -
-
-
-
-
-
- -
-

- Sorry for the wait -

-
-
-
-
-
-
-

- - - If your transaction is not finalized within -

- 0:00 -

- , it will be canceled and you will not be charged for gas. - - -

-
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "cancelled" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "deadline_missed" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction was canceled -

-
-

- Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "pending" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "reverted" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "success" STX status for a dapp transaction 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction is complete -

-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the "unknown" STX status 1`] = ` -
-
-
-
-
-
- -
-

- Your transaction failed -

-
-

- Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. -

-
-
- -
-
-
-
- -
-
-`; - -exports[`SmartTransactionStatusPage renders the component with initial props 1`] = ` -
-
-
-
-
-
- -
-

- Submitting your transaction -

-
-
-
-
-
-
-

- - - Estimated completion in < -

- 0:45 -

- - - -

-
-
-
-
- -
-
-`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap new file mode 100644 index 000000000000..f3ff42c89116 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/__snapshots__/smart-transactions-status-page.test.tsx.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SmartTransactionStatusPage renders the "failed" STX status: smart-transaction-status-failed 1`] = ` +
+
+
+
+
+

+ Your transaction failed +

+
+

+ Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support. +

+
+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "pending" STX status: smart-transaction-status-pending 1`] = ` +
+
+
+
+
+

+ Your transaction was submitted +

+
+ +
+
+
+ +
+
+`; + +exports[`SmartTransactionStatusPage renders the "success" STX status: smart-transaction-status-success 1`] = ` +
+
+
+
+
+

+ Your transaction is complete +

+
+ +
+
+
+ +
+
+`; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss index 5e74ba9a8b3d..2227673029d8 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/index.scss +++ b/ui/pages/smart-transactions/smart-transaction-status-page/index.scss @@ -1,10 +1,3 @@ - -@keyframes shift { - to { - background-position: 100% 0; - } -} - .smart-transaction-status-page { text-align: center; @@ -20,24 +13,6 @@ } } - &__loading-bar-container { - @media screen and (min-width: 768px) { - max-width: 260px; - } - - width: 100%; - height: 3px; - background: var(--color-background-alternative); - display: flex; - margin-top: 16px; - } - - &__loading-bar { - height: 3px; - background: var(--color-primary-default); - transition: width 0.5s linear; - } - &__footer { grid-area: footer; } @@ -45,35 +20,4 @@ &__countdown { width: 25px; } - - // Slightly overwrite the default SimulationDetails layout to look better on the Smart Transaction status page. - .simulation-details-layout { - margin-left: 0; - margin-right: 0; - width: 100%; - text-align: left; - } - - &__background-animation { - position: relative; - left: -88px; - background-repeat: repeat; - background-position: 0 0; - - &--top { - width: 1634px; - height: 54px; - background-size: 817px 54px; - background-image: url('/images/transaction-background-top.svg'); - animation: shift 19s linear infinite; - } - - &--bottom { - width: 1600px; - height: 62px; - background-size: 800px 62px; - background-image: url('/images/transaction-background-bottom.svg'); - animation: shift 22s linear infinite; - } - } } diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx new file mode 100644 index 000000000000..fa4166af1461 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.test.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +// Declare a variable to store the onComplete callback +let mockOnComplete: () => void; + +// Modify the existing jest.mock to capture the onComplete callback +jest.mock('../../../components/component-library/lottie-animation', () => ({ + LottieAnimation: ({ + path, + loop, + autoplay, + onComplete, + }: { + path: string; + loop: boolean; + autoplay: boolean; + onComplete: () => void; + }) => { + // Store the onComplete callback for later use in tests + mockOnComplete = onComplete; + return ( +
+ ); + }, +})); + +describe('SmartTransactionsStatusAnimation', () => { + it('renders correctly for PENDING status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for SUCCESS status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('confirmed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for REVERTED status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for UNKNOWN status', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('failed'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + }); + + it('renders correctly for other statuses', () => { + const { getByTestId } = render( + , + ); + const lottieAnimation = getByTestId('mock-lottie-animation'); + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('processing'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); + + it('transitions from submittingIntro to submittingLoop when onComplete is called', () => { + render( + , + ); + const lottieAnimation = screen.getByTestId('mock-lottie-animation'); + + // Initially, should render 'submitting-intro' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-intro'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'false'); + + // Trigger the onComplete callback to simulate animation completion + expect(lottieAnimation.getAttribute('data-on-complete')).toBeDefined(); + act(() => { + mockOnComplete(); + }); + + // After onComplete is called, it should transition to 'submitting-loop' + expect(lottieAnimation).toHaveAttribute( + 'data-path', + expect.stringContaining('submitting-loop'), + ); + expect(lottieAnimation).toHaveAttribute('data-loop', 'true'); + }); +}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx new file mode 100644 index 000000000000..3dc739aefa1f --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-animation.tsx @@ -0,0 +1,80 @@ +import React, { useState, useCallback } from 'react'; +import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; +import { Box } from '../../../components/component-library'; +import { Display } from '../../../helpers/constants/design-system'; +import { LottieAnimation } from '../../../components/component-library/lottie-animation'; + +const ANIMATIONS_FOLDER = 'images/animations/smart-transaction-status'; + +type AnimationInfo = { + path: string; + loop: boolean; +}; + +const Animations: Record = { + Failed: { + path: `${ANIMATIONS_FOLDER}/failed.lottie.json`, + loop: false, + }, + Confirmed: { + path: `${ANIMATIONS_FOLDER}/confirmed.lottie.json`, + loop: false, + }, + SubmittingIntro: { + path: `${ANIMATIONS_FOLDER}/submitting-intro.lottie.json`, + loop: false, + }, + SubmittingLoop: { + path: `${ANIMATIONS_FOLDER}/submitting-loop.lottie.json`, + loop: true, + }, + Processing: { + path: `${ANIMATIONS_FOLDER}/processing.lottie.json`, + loop: true, + }, +}; + +export const SmartTransactionStatusAnimation = ({ + status, +}: { + status: SmartTransactionStatuses; +}) => { + const [isIntro, setIsIntro] = useState(true); + + let animation: AnimationInfo; + + if (status === SmartTransactionStatuses.PENDING) { + animation = isIntro + ? Animations.SubmittingIntro + : Animations.SubmittingLoop; + } else { + switch (status) { + case SmartTransactionStatuses.SUCCESS: + animation = Animations.Confirmed; + break; + case SmartTransactionStatuses.REVERTED: + case SmartTransactionStatuses.UNKNOWN: + animation = Animations.Failed; + break; + default: + animation = Animations.Processing; + } + } + + const handleAnimationComplete = useCallback(() => { + if (status === SmartTransactionStatuses.PENDING && isIntro) { + setIsIntro(false); + } + }, [status, isIntro]); + + return ( + + + + ); +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx new file mode 100644 index 000000000000..12d356ce4cc4 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.stories.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import SmartTransactionStatusPage from './smart-transaction-status-page'; +import { Meta, StoryObj } from '@storybook/react'; +import { SimulationData } from '@metamask/transaction-controller'; +import { mockNetworkState } from '../../../../test/stub/networks'; + +// Mock data +const CHAIN_ID_MOCK = '0x1'; + +const simulationData: SimulationData = { + nativeBalanceChange: { + previousBalance: '0x0', + newBalance: '0x0', + difference: '0x12345678912345678', + isDecrease: true, + }, + tokenBalanceChanges: [], +}; + +const TX_MOCK = { + id: 'txId', + simulationData, + chainId: CHAIN_ID_MOCK, +}; + +const storeMock = configureStore({ + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: false, + }, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + transactions: [TX_MOCK], + currentNetworkTxList: [TX_MOCK], + }, +}); + +const meta: Meta = { + title: 'Pages/SmartTransactions/SmartTransactionStatusPage', + component: SmartTransactionStatusPage, + decorators: [(story) => {story()}], +}; + +export default meta; +type Story = StoryObj; + +export const Pending: Story = { + args: { + requestState: { + smartTransaction: { + status: 'pending', + creationTime: Date.now(), + uuid: 'uuid', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Success: Story = { + args: { + requestState: { + smartTransaction: { + status: 'success', + creationTime: Date.now() - 60000, // 1 minute ago + uuid: 'uuid-success', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-success', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; + +export const Failed: Story = { + args: { + requestState: { + smartTransaction: { + status: 'unknown', + creationTime: Date.now() - 180000, // 3 minutes ago + uuid: 'uuid-failed', + chainId: '0x1', + }, + isDapp: false, + txId: 'txId-failed', + }, + onCloseExtension: () => {}, + onViewActivity: () => {}, + }, +}; diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx index 2eb29bfa4e4e..4492ed4e4844 100644 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transaction-status-page.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { SmartTransactionStatuses, @@ -8,9 +8,7 @@ import { import { Box, Text, - Icon, IconName, - IconSize, Button, ButtonVariant, ButtonSecondary, @@ -26,22 +24,18 @@ import { TextColor, FontWeight, IconColor, - TextAlign, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentChainId, getFullTxData } from '../../../selectors'; -import { getFeatureFlagsByChainId } from '../../../../shared/modules/selectors'; import { BaseUrl } from '../../../../shared/constants/urls'; -import { - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE, - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE, -} from '../../../../shared/constants/smartTransactions'; import { hideLoadingIndication } from '../../../store/actions'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { SimulationDetails } from '../../confirmations/components/simulation-details'; import { NOTIFICATION_WIDTH } from '../../../../shared/constants/notifications'; -type RequestState = { +import { SmartTransactionStatusAnimation } from './smart-transaction-status-animation'; + +export type RequestState = { smartTransaction?: SmartTransaction; isDapp: boolean; txId?: string; @@ -49,8 +43,8 @@ type RequestState = { export type SmartTransactionStatusPageProps = { requestState: RequestState; - onCloseExtension: () => void; - onViewActivity: () => void; + onCloseExtension?: () => void; + onViewActivity?: () => void; }; export const showRemainingTimeInMinAndSec = ( @@ -66,30 +60,18 @@ export const showRemainingTimeInMinAndSec = ( const getDisplayValues = ({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }: { t: ReturnType; - countdown: JSX.Element | undefined; isSmartTransactionPending: boolean; - isSmartTransactionTakingTooLong: boolean; isSmartTransactionSuccess: boolean; isSmartTransactionCancelled: boolean; }) => { - if (isSmartTransactionPending && isSmartTransactionTakingTooLong) { - return { - title: t('smartTransactionTakingTooLong'), - description: t('smartTransactionTakingTooLongDescription', [countdown]), - iconName: IconName.Clock, - iconColor: IconColor.primaryDefault, - }; - } else if (isSmartTransactionPending) { + if (isSmartTransactionPending) { return { title: t('smartTransactionPending'), - description: t('stxEstimatedCompletion', [countdown]), iconName: IconName.Clock, iconColor: IconColor.primaryDefault, }; @@ -102,7 +84,7 @@ const getDisplayValues = ({ } else if (isSmartTransactionCancelled) { return { title: t('smartTransactionCancelled'), - description: t('smartTransactionCancelledDescription', [countdown]), + description: t('smartTransactionCancelledDescription'), iconName: IconName.Danger, iconColor: IconColor.errorDefault, }; @@ -116,98 +98,6 @@ const getDisplayValues = ({ }; }; -const useRemainingTime = ({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, -}: { - isSmartTransactionPending: boolean; - smartTransaction?: SmartTransaction; - stxMaxDeadline: number; - stxEstimatedDeadline: number; -}) => { - const [timeLeftForPendingStxInSec, setTimeLeftForPendingStxInSec] = - useState(0); - const [isSmartTransactionTakingTooLong, setIsSmartTransactionTakingTooLong] = - useState(false); - const stxDeadline = isSmartTransactionTakingTooLong - ? stxMaxDeadline - : stxEstimatedDeadline; - - useEffect(() => { - if (!isSmartTransactionPending) { - return; - } - - const calculateRemainingTime = () => { - const secondsAfterStxSubmission = smartTransaction?.creationTime - ? Math.round((Date.now() - smartTransaction.creationTime) / 1000) - : 0; - - if (secondsAfterStxSubmission > stxDeadline) { - setTimeLeftForPendingStxInSec(0); - if (!isSmartTransactionTakingTooLong) { - setIsSmartTransactionTakingTooLong(true); - } - return; - } - - setTimeLeftForPendingStxInSec(stxDeadline - secondsAfterStxSubmission); - }; - - const intervalId = setInterval(calculateRemainingTime, 1000); - calculateRemainingTime(); - - // eslint-disable-next-line consistent-return - return () => clearInterval(intervalId); - }, [ - isSmartTransactionPending, - isSmartTransactionTakingTooLong, - smartTransaction?.creationTime, - stxDeadline, - ]); - - return { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - }; -}; - -const Deadline = ({ - isSmartTransactionPending, - stxDeadline, - timeLeftForPendingStxInSec, -}: { - isSmartTransactionPending: boolean; - stxDeadline: number; - timeLeftForPendingStxInSec: number; -}) => { - if (!isSmartTransactionPending) { - return null; - } - return ( - -
-
-
- - ); -}; - const Description = ({ description }: { description: string | undefined }) => { if (!description) { return null; @@ -388,29 +278,10 @@ const Title = ({ title }: { title: string }) => { ); }; -const SmartTransactionsStatusIcon = ({ - iconName, - iconColor, -}: { - iconName: IconName; - iconColor: IconColor; -}) => { - return ( - - - - ); -}; - export const SmartTransactionStatusPage = ({ requestState, - onCloseExtension, - onViewActivity, + onCloseExtension = () => null, + onViewActivity = () => null, }: SmartTransactionStatusPageProps) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -423,50 +294,15 @@ export const SmartTransactionStatusPage = ({ const isSmartTransactionCancelled = Boolean( smartTransaction?.status?.startsWith(SmartTransactionStatuses.CANCELLED), ); - const featureFlags: { - smartTransactions?: { - expectedDeadline?: number; - maxDeadline?: number; - }; - } | null = useSelector(getFeatureFlagsByChainId); - const stxEstimatedDeadline = - featureFlags?.smartTransactions?.expectedDeadline || - FALLBACK_SMART_TRANSACTIONS_EXPECTED_DEADLINE; - const stxMaxDeadline = - featureFlags?.smartTransactions?.maxDeadline || - FALLBACK_SMART_TRANSACTIONS_MAX_DEADLINE; - const { - timeLeftForPendingStxInSec, - isSmartTransactionTakingTooLong, - stxDeadline, - } = useRemainingTime({ - isSmartTransactionPending, - smartTransaction, - stxMaxDeadline, - stxEstimatedDeadline, - }); + const chainId: string = useSelector(getCurrentChainId); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: This same selector is used in the awaiting-swap component. const fullTxData = useSelector((state) => getFullTxData(state, txId)) || {}; - const countdown = isSmartTransactionPending ? ( - - {showRemainingTimeInMinAndSec(timeLeftForPendingStxInSec)} - - ) : undefined; - - const { title, description, iconName, iconColor } = getDisplayValues({ + const { title, description } = getDisplayValues({ t, - countdown, isSmartTransactionPending, - isSmartTransactionTakingTooLong, isSmartTransactionSuccess, isSmartTransactionCancelled, }); @@ -515,20 +351,10 @@ export const SmartTransactionStatusPage = ({ paddingRight={6} width={BlockSize.Full} > - - - <Deadline - isSmartTransactionPending={isSmartTransactionPending} - stxDeadline={stxDeadline} - timeLeftForPendingStxInSec={timeLeftForPendingStxInSec} - /> <Description description={description} /> <PortfolioSmartTransactionStatusUrl portfolioSmartTransactionStatusUrl={ @@ -539,15 +365,13 @@ export const SmartTransactionStatusPage = ({ /> </Box> {canShowSimulationDetails && ( - <SimulationDetails - simulationData={fullTxData.simulationData} - transactionId={fullTxData.id} - /> + <Box width={BlockSize.Full}> + <SimulationDetails + simulationData={fullTxData.simulationData} + transactionId={fullTxData.id} + /> + </Box> )} - <Box - marginTop={3} - className="smart-transaction-status-page__background-animation smart-transaction-status-page__background-animation--bottom" - /> </Box> <SmartTransactionsStatusPageFooter isDapp={isDapp} diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js deleted file mode 100644 index d014c56373a4..000000000000 --- a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.js +++ /dev/null @@ -1,226 +0,0 @@ -import React from 'react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { SmartTransactionStatuses } from '@metamask/smart-transactions-controller/dist/types'; - -import { - renderWithProvider, - createSwapsMockStore, -} from '../../../../test/jest'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { SmartTransactionStatusPage } from '.'; - -const middleware = [thunk]; - -describe('SmartTransactionStatusPage', () => { - const requestState = { - smartTransaction: { - status: SmartTransactionStatuses.PENDING, - creationTime: Date.now(), - }, - }; - - it('renders the component with initial props', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Submitting your transaction')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "Sorry for the wait" pending status', () => { - const store = configureMockStore(middleware)(createSwapsMockStore()); - const newRequestState = { - ...requestState, - smartTransaction: { - ...requestState.smartTransaction, - creationTime: 1519211809934, - }, - }; - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={newRequestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('Sorry for the wait')).toBeInTheDocument(); - expect(queryByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction is complete')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "reverted" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.REVERTED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect( - getByText( - 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', - ), - ).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - requestState.smartTransaction = latestSmartTransaction; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect( - getByText( - `Your transaction couldn't be completed, so it was canceled to save you from paying unnecessary gas fees.`, - ), - ).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "deadline_missed" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = - SmartTransactionStatuses.CANCELLED_DEADLINE_MISSED; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction was canceled')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "unknown" STX status', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.UNKNOWN; - requestState.smartTransaction = latestSmartTransaction; - const store = configureMockStore(middleware)(mockStore); - const { getByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect(getByText('Your transaction failed')).toBeInTheDocument(); - expect(getByText('View transaction')).toBeInTheDocument(); - expect(getByText('View activity')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "pending" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.PENDING; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(queryByText('View activity')).not.toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "success" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.SUCCESS; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); - - it('renders the "cancelled" STX status for a dapp transaction', () => { - const mockStore = createSwapsMockStore(); - const latestSmartTransaction = - mockStore.metamask.smartTransactionsState.smartTransactions[ - CHAIN_IDS.MAINNET - ][1]; - latestSmartTransaction.status = SmartTransactionStatuses.CANCELLED; - requestState.smartTransaction = latestSmartTransaction; - requestState.isDapp = true; - const store = configureMockStore(middleware)(mockStore); - const { queryByText, container } = renderWithProvider( - <SmartTransactionStatusPage requestState={requestState} />, - store, - ); - expect( - queryByText('You may close this window anytime.'), - ).not.toBeInTheDocument(); - expect(queryByText('View transaction')).toBeInTheDocument(); - expect(queryByText('Close extension')).toBeInTheDocument(); - expect(container).toMatchSnapshot(); - }); -}); diff --git a/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx new file mode 100644 index 000000000000..afd9b2872ce1 --- /dev/null +++ b/ui/pages/smart-transactions/smart-transaction-status-page/smart-transactions-status-page.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { + SmartTransaction, + SmartTransactionStatuses, +} from '@metamask/smart-transactions-controller/dist/types'; + +import { fireEvent } from '@testing-library/react'; +import { + renderWithProvider, + createSwapsMockStore, +} from '../../../../test/jest'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; +import { + SmartTransactionStatusPage, + RequestState, +} from './smart-transaction-status-page'; + +// Mock the SmartTransactionStatusAnimation component and capture props +jest.mock('./smart-transaction-status-animation', () => ({ + SmartTransactionStatusAnimation: ({ + status, + }: { + status: SmartTransactionStatuses; + }) => <div data-testid="mock-animation" data-status={status} />, +})); + +const middleware = [thunk]; +const mockStore = configureMockStore(middleware); + +const defaultRequestState: RequestState = { + smartTransaction: { + status: SmartTransactionStatuses.PENDING, + creationTime: Date.now(), + uuid: 'uuid', + chainId: CHAIN_IDS.MAINNET, + }, + isDapp: false, + txId: 'txId', +}; + +describe('SmartTransactionStatusPage', () => { + const statusTestCases = [ + { + status: SmartTransactionStatuses.PENDING, + isDapp: false, + expectedTexts: ['Your transaction was submitted', 'View activity'], + snapshotName: 'pending', + }, + { + status: SmartTransactionStatuses.SUCCESS, + isDapp: false, + expectedTexts: [ + 'Your transaction is complete', + 'View transaction', + 'View activity', + ], + snapshotName: 'success', + }, + { + status: SmartTransactionStatuses.REVERTED, + isDapp: false, + expectedTexts: [ + 'Your transaction failed', + 'View transaction', + 'View activity', + 'Sudden market changes can cause failures. If the problem continues, reach out to MetaMask customer support.', + ], + snapshotName: 'failed', + }, + ]; + + statusTestCases.forEach(({ status, isDapp, expectedTexts, snapshotName }) => { + it(`renders the "${snapshotName}" STX status${ + isDapp ? ' for a dapp transaction' : '' + }`, () => { + const state = createSwapsMockStore(); + const latestSmartTransaction = + state.metamask.smartTransactionsState.smartTransactions[ + CHAIN_IDS.MAINNET + ][1]; + latestSmartTransaction.status = status; + const requestState: RequestState = { + smartTransaction: latestSmartTransaction as SmartTransaction, + isDapp, + txId: 'txId', + }; + + const { getByText, getByTestId, container } = renderWithProvider( + <SmartTransactionStatusPage requestState={requestState} />, + mockStore(state), + ); + + expectedTexts.forEach((text) => { + expect(getByText(text)).toBeInTheDocument(); + }); + + expect(getByTestId('mock-animation')).toBeInTheDocument(); + expect(getByTestId('mock-animation')).toHaveAttribute( + 'data-status', + status, + ); + expect(container).toMatchSnapshot( + `smart-transaction-status-${snapshotName}`, + ); + }); + }); + + describe('Action Buttons', () => { + it('calls onCloseExtension when Close extension button is clicked', () => { + const onCloseExtension = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: true }} + onCloseExtension={onCloseExtension} + />, + store, + ); + + const closeButton = getByText('Close extension'); + fireEvent.click(closeButton); + expect(onCloseExtension).toHaveBeenCalled(); + }); + + it('calls onViewActivity when View activity button is clicked', () => { + const onViewActivity = jest.fn(); + const store = mockStore(createSwapsMockStore()); + + const { getByText } = renderWithProvider( + <SmartTransactionStatusPage + requestState={{ ...defaultRequestState, isDapp: false }} + onViewActivity={onViewActivity} + />, + store, + ); + + const viewActivityButton = getByText('View activity'); + fireEvent.click(viewActivityButton); + expect(onViewActivity).toHaveBeenCalled(); + }); + }); +}); From 11ca25b78635455023ab23a2fc1e3544e6284cbc Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:51:37 -0400 Subject: [PATCH 09/12] feat: Adding delete metametrics data to security and privacy tab (#24571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> **This PR is dependant on #24503** ## **Description** - Added a new functional component as an entry to the Security & Privacy tab with the `Delete MetaMetrics Data` button. - A new Delete MetaMetrics Data model will open when you click the button. - Clicking the `Clear` button in the modal will create a data deletion regulation, update the state, and close the modal, deactivating the `Delete MetaMetrics Data` button. - The Erroring on the `Clear` button click opens a new error modal. **Scenarios to disable the DeleteMetaMetrics button:** 1. Metametrics ID not created / not available 2. Just performed a deletion independent on participate in metametrics toggle 3. Participate in metric opt-out & no data is recorded after deletion. 4. Status of current delete regulation as INITIALIZED, RUNNING, or FINISHED and (Participate in metric opt-out/no data recorded after deletion) <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24571?quickstart=1) ## **Related issues** Fixes #24406, #24407, https://github.com/MetaMask/MetaMask-planning/issues/2523 ## **Manual testing steps** Perquisite: Provide the following details in the `.metamaskrc` file: ``` ANALYTICS_DATA_DELETION_SOURCE_ID="wygFTooEUUtcckty9kaMc" ANALYTICS_DATA_DELETION_ENDPOINT="https://proxy.dev-api.cx.metamask.io/segment/v1" ``` 1. Make a build(`yarn`, `yarn dist`) against the code. 2. Load the extension in any browser. 3. Navigate to the "Security & privacy" in the Settings 4. Click on the "Delete MetaMetrics data" button which enables when the "Participate in MetaMetrics" is selected. 5. Validate the post request is made in the service worker with the id - `wygFTooEUUtcckty9kaMc`. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: seaona <54408225+seaona@users.noreply.github.com> --- app/_locales/en/messages.json | 26 ++ privacy-snapshot.json | 2 + shared/constants/metametrics.ts | 2 + .../metrics/delete-metametrics-data.spec.ts | 246 ++++++++++++++++++ test/e2e/webdriver/driver.js | 2 + test/e2e/webdriver/types.ts | 5 + .../clear-metametrics-data.test.tsx | 59 +++++ .../clear-metametrics-data.tsx | 130 +++++++++ .../app/clear-metametrics-data/index.ts | 1 + .../data-deletion-error-modal.test.tsx | 51 ++++ .../data-deletion-error-modal.tsx | 99 +++++++ .../app/data-deletion-error-modal/index.ts | 1 + ui/ducks/app/app.test.js | 38 +++ ui/ducks/app/app.ts | 48 ++++ ui/helpers/constants/settings.js | 7 + ui/helpers/utils/settings-search.test.js | 2 +- .../__snapshots__/security-tab.test.js.snap | 53 ++++ .../delete-metametrics-data-button.test.tsx | 212 +++++++++++++++ .../delete-metametrics-data-button.tsx | 147 +++++++++++ .../delete-metametrics-data-button/index.ts | 1 + .../security-tab/security-tab.component.js | 13 +- .../security-tab/security-tab.container.js | 6 + .../security-tab/security-tab.test.js | 26 ++ ui/selectors/metametrics.js | 3 + ui/selectors/metametrics.test.js | 12 + ui/selectors/selectors.js | 20 ++ ui/selectors/selectors.test.js | 62 +++++ ui/store/actionConstants.ts | 8 + 28 files changed, 1278 insertions(+), 4 deletions(-) create mode 100644 test/e2e/tests/metrics/delete-metametrics-data.spec.ts create mode 100644 test/e2e/webdriver/types.ts create mode 100644 ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx create mode 100644 ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx create mode 100644 ui/components/app/clear-metametrics-data/index.ts create mode 100644 ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx create mode 100644 ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx create mode 100644 ui/components/app/data-deletion-error-modal/index.ts create mode 100644 ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx create mode 100644 ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx create mode 100644 ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 9a94edeb7edf..de17cf4ea877 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1569,6 +1569,29 @@ "deleteContact": { "message": "Delete contact" }, + "deleteMetaMetricsData": { + "message": "Delete MetaMetrics data" + }, + "deleteMetaMetricsDataDescription": { + "message": "This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our $1.", + "description": "$1 will have text saying Privacy Policy " + }, + "deleteMetaMetricsDataErrorDesc": { + "message": "This request can't be completed right now due to an analytics system server issue, please try again later" + }, + "deleteMetaMetricsDataErrorTitle": { + "message": "We are unable to delete this data right now" + }, + "deleteMetaMetricsDataModalDesc": { + "message": "We are about to remove all your MetaMetrics data. Are you sure?" + }, + "deleteMetaMetricsDataModalTitle": { + "message": "Delete MetaMetrics data?" + }, + "deleteMetaMetricsDataRequestedDescription": { + "message": "You initiated this action on $1. This process can take up to 30 days. View the $2", + "description": "$1 will be the date on which teh deletion is requested and $2 will have text saying Privacy Policy " + }, "deleteNetworkIntro": { "message": "If you delete this network, you will need to add it again to view your assets in this network" }, @@ -2873,6 +2896,9 @@ "metaMaskConnectStatusParagraphTwo": { "message": "The connection status button shows if the website you’re visiting is connected to your currently selected account." }, + "metaMetricsIdNotAvailableError": { + "message": "Since you've never opted into MetaMetrics, there's no data to delete here." + }, "metadataModalSourceTooltip": { "message": "$1 is hosted on npm and $2 is this Snap’s unique identifier.", "description": "$1 is the snap name and $2 is the snap NPM id." diff --git a/privacy-snapshot.json b/privacy-snapshot.json index b8920724a597..2516654f1803 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -33,6 +33,7 @@ "mainnet.infura.io", "metamask.eth", "metamask.github.io", + "metametrics.metamask.test", "min-api.cryptocompare.com", "nft.api.cx.metamask.io", "oidc.api.cx.metamask.io", @@ -42,6 +43,7 @@ "portfolio.metamask.io", "price.api.cx.metamask.io", "proxy.api.cx.metamask.io", + "proxy.dev-api.cx.metamask.io", "raw.githubusercontent.com", "registry.npmjs.org", "responsive-rpc.test", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 945af5416057..d0f1cfb87cbe 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -536,6 +536,7 @@ export enum MetaMetricsEventName { EncryptionPublicKeyApproved = 'Encryption Approved', EncryptionPublicKeyRejected = 'Encryption Rejected', EncryptionPublicKeyRequested = 'Encryption Requested', + ErrorOccured = 'Error occured', ExternalLinkClicked = 'External Link Clicked', KeyExportSelected = 'Key Export Selected', KeyExportRequested = 'Key Export Requested', @@ -552,6 +553,7 @@ export enum MetaMetricsEventName { MarkAllNotificationsRead = 'Notifications Marked All as Read', MetricsOptIn = 'Metrics Opt In', MetricsOptOut = 'Metrics Opt Out', + MetricsDataDeletionRequest = 'Delete MetaMetrics Data Request Submitted', NavAccountMenuOpened = 'Account Menu Opened', NavConnectedSitesOpened = 'Connected Sites Opened', NavMainMenuOpened = 'Main Menu Opened', diff --git a/test/e2e/tests/metrics/delete-metametrics-data.spec.ts b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts new file mode 100644 index 000000000000..308ff8508d0a --- /dev/null +++ b/test/e2e/tests/metrics/delete-metametrics-data.spec.ts @@ -0,0 +1,246 @@ +import { strict as assert } from 'assert'; +import { MockedEndpoint, Mockttp } from 'mockttp'; +import { Suite } from 'mocha'; +import { + defaultGanacheOptions, + withFixtures, + getEventPayloads, + unlockWallet, +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { TestSuiteArguments } from '../confirmations/transactions/shared'; +import { WebElementWithWaitForElementState } from '../../webdriver/types'; + +const selectors = { + accountOptionsMenuButton: '[data-testid="account-options-menu-button"]', + globalMenuSettingsButton: '[data-testid="global-menu-settings"]', + securityAndPrivacySettings: { text: 'Security & privacy', tag: 'div' }, + experimentalSettings: { text: 'Experimental', tag: 'div' }, + deletMetaMetricsSettings: '[data-testid="delete-metametrics-data-button"]', + deleteMetaMetricsDataButton: { + text: 'Delete MetaMetrics data', + tag: 'button', + }, + clearButton: { text: 'Clear', tag: 'button' }, + backButton: '[data-testid="settings-back-button"]', +}; + +/** + * mocks the segment api multiple times for specific payloads that we expect to + * see when these tests are run. In this case we are looking for + * 'Permissions Requested' and 'Permissions Received'. Do not use the constants + * from the metrics constants files, because if these change we want a strong + * indicator to our data team that the shape of data will change. + * + * @param mockServer + * @returns + */ +const mockSegment = async (mockServer: Mockttp) => { + return [ + await mockServer + .forPost('https://api.segment.io/v1/batch') + .withJsonBodyIncluding({ + batch: [ + { type: 'track', event: 'Delete MetaMetrics Data Request Submitted' }, + ], + }) + .thenCallback(() => { + return { + statusCode: 200, + }; + }), + await mockServer + .forPost('https://metametrics.metamask.test/regulations/sources/test') + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .withBodyIncluding( + JSON.stringify({ + regulationType: 'DELETE_ONLY', + subjectType: 'USER_ID', + subjectIds: ['fake-metrics-id'], + }), + ) + .thenCallback(() => ({ + statusCode: 200, + json: { data: { regulateId: 'fake-delete-regulation-id' } }, + })), + await mockServer + .forGet( + 'https://metametrics.metamask.test/regulations/fake-delete-regulation-id', + ) + .withHeaders({ 'Content-Type': 'application/vnd.segment.v1+json' }) + .thenCallback(() => ({ + statusCode: 200, + json: { + data: { + regulation: { + overallStatus: 'FINISHED', + }, + }, + }, + })), + ]; +}; +/** + * Scenarios: + * 1. Deletion while Metrics is Opted in. + * 2. Deletion while Metrics is Opted out. + * 3. Deletion when user never opted for metrics. + */ +describe('Delete MetaMetrics Data @no-mmi', function (this: Suite) { + it('while user has opted in for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 3); + assert.deepStrictEqual(events[0].properties, { + category: 'Settings', + locale: 'en', + chain_id: '0x539', + environment_type: 'fullscreen', + }); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = + await driver.findClickableElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButtonRefreshed.isEnabled(), + true, + 'Delete MetaMetrics data button is enabled', + ); + }, + ); + }); + it('while user has opted out for metrics tracking', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + }) + .build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + await driver.findElement(selectors.deletMetaMetricsSettings); + await driver.clickElement(selectors.deleteMetaMetricsDataButton); + + // there is a race condition, where we need to wait before clicking clear button otherwise an error is thrown in the background + // we cannot wait for a UI conditon, so we a delay to mitigate this until another solution is found + await driver.delay(3000); + await driver.clickElementAndWaitToDisappear(selectors.clearButton); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButton as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + + const events = await getEventPayloads( + driver, + mockedEndpoints as MockedEndpoint[], + ); + assert.equal(events.length, 2); + + await driver.clickElementAndWaitToDisappear( + '.mm-box button[aria-label="Close"]', + ); + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + + const deleteMetaMetricsDataButtonRefreshed = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + await ( + deleteMetaMetricsDataButtonRefreshed as WebElementWithWaitForElementState + ).waitForElementState('disabled'); + }, + ); + }); + it('when the user has never opted in for metrics', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + defaultGanacheOptions, + title: this.test?.fullTitle(), + testSpecificMock: mockSegment, + }, + async ({ driver }: { driver: Driver }) => { + await unlockWallet(driver); + + await driver.clickElement(selectors.accountOptionsMenuButton); + await driver.clickElement(selectors.globalMenuSettingsButton); + await driver.clickElement(selectors.securityAndPrivacySettings); + await driver.findElement(selectors.deletMetaMetricsSettings); + + const deleteMetaMetricsDataButton = await driver.findElement( + selectors.deleteMetaMetricsDataButton, + ); + assert.equal( + await deleteMetaMetricsDataButton.isEnabled(), + false, + 'Delete MetaMetrics data button is disabled', + ); + }, + ); + }); +}); diff --git a/test/e2e/webdriver/driver.js b/test/e2e/webdriver/driver.js index 97c616d01f4b..a03a0d1cbd04 100644 --- a/test/e2e/webdriver/driver.js +++ b/test/e2e/webdriver/driver.js @@ -63,6 +63,8 @@ function wrapElementWithAPI(element, driver) { return await driver.wait(until.stalenessOf(element), timeout); case 'visible': return await driver.wait(until.elementIsVisible(element), timeout); + case 'disabled': + return await driver.wait(until.elementIsDisabled(element), timeout); default: throw new Error(`Provided state: '${state}' is not supported`); } diff --git a/test/e2e/webdriver/types.ts b/test/e2e/webdriver/types.ts new file mode 100644 index 000000000000..68cfa15dd600 --- /dev/null +++ b/test/e2e/webdriver/types.ts @@ -0,0 +1,5 @@ +import { WebElement, WebElementPromise } from 'selenium-webdriver'; + +export type WebElementWithWaitForElementState = WebElement & { + waitForElementState: (state: unknown, timeout?: unknown) => WebElementPromise; +}; diff --git a/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx new file mode 100644 index 000000000000..5ce4ac7573dc --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import * as Actions from '../../../store/actions'; +import { DELETE_METAMETRICS_DATA_MODAL_CLOSE } from '../../../store/actionConstants'; +import ClearMetaMetricsData from './clear-metametrics-data'; + +const mockCloseDeleteMetaMetricsDataModal = jest.fn().mockImplementation(() => { + return { + type: DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }; +}); + +jest.mock('../../../store/actions', () => ({ + createMetaMetricsDataDeletionTask: jest.fn(), +})); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDeleteMetaMetricsDataModal: () => { + return mockCloseDeleteMetaMetricsDataModal(); + }, + }; +}); + +describe('ClearMetaMetricsData', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render the data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<ClearMetaMetricsData />, store); + + expect(getByText('Delete MetaMetrics data?')).toBeInTheDocument(); + expect( + getByText( + 'We are about to remove all your MetaMetrics data. Are you sure?', + ), + ).toBeInTheDocument(); + }); + + it('should call createMetaMetricsDataDeletionTask when Clear button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<ClearMetaMetricsData />, store); + expect(getByText('Clear')).toBeEnabled(); + fireEvent.click(getByText('Clear')); + expect(Actions.createMetaMetricsDataDeletionTask).toHaveBeenCalledTimes(1); + }); + + it('should call hideDeleteMetaMetricsDataModal when Cancel button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<ClearMetaMetricsData />, store); + expect(getByText('Cancel')).toBeEnabled(); + fireEvent.click(getByText('Cancel')); + expect(mockCloseDeleteMetaMetricsDataModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx new file mode 100644 index 000000000000..019c115eceac --- /dev/null +++ b/ui/components/app/clear-metametrics-data/clear-metametrics-data.tsx @@ -0,0 +1,130 @@ +import React, { useContext } from 'react'; +import { useDispatch } from 'react-redux'; +import { + hideDeleteMetaMetricsDataModal, + openDataDeletionErrorModal, +} from '../../../ducks/app/app'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + Box, + Button, + ButtonSize, + ButtonVariant, + Modal, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, +} from '../../component-library'; +import { + AlignItems, + BlockSize, + Display, + FlexDirection, + JustifyContent, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { createMetaMetricsDataDeletionTask } from '../../../store/actions'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; + +export default function ClearMetaMetricsData() { + const t = useI18nContext(); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + + const closeModal = () => { + dispatch(hideDeleteMetaMetricsDataModal()); + }; + + const deleteMetaMetricsData = async () => { + try { + await createMetaMetricsDataDeletionTask(); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.MetricsDataDeletionRequest, + }, + { + excludeMetaMetricsId: true, + }, + ); + } catch (error: unknown) { + dispatch(openDataDeletionErrorModal()); + trackEvent( + { + category: MetaMetricsEventCategory.Settings, + event: MetaMetricsEventName.ErrorOccured, + }, + { + excludeMetaMetricsId: true, + }, + ); + } finally { + dispatch(hideDeleteMetaMetricsDataModal()); + } + }; + + return ( + <Modal isOpen onClose={closeModal}> + <ModalOverlay /> + <ModalContent + modalDialogProps={{ + display: Display.Flex, + flexDirection: FlexDirection.Column, + }} + > + <ModalHeader onClose={closeModal}> + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} + > + <Text variant={TextVariant.headingSm}> + {t('deleteMetaMetricsDataModalTitle')} + </Text> + </Box> + </ModalHeader> + <Box + marginLeft={4} + marginRight={4} + marginBottom={3} + display={Display.Flex} + flexDirection={FlexDirection.Column} + gap={4} + > + <Text variant={TextVariant.bodySmMedium}> + {t('deleteMetaMetricsDataModalDesc')} + </Text> + </Box> + <ModalFooter> + <Box display={Display.Flex} gap={4}> + <Button + size={ButtonSize.Lg} + width={BlockSize.Half} + variant={ButtonVariant.Secondary} + onClick={closeModal} + > + {t('cancel')} + </Button> + <Button + data-testid="clear-metametrics-data" + size={ButtonSize.Lg} + width={BlockSize.Half} + variant={ButtonVariant.Primary} + onClick={deleteMetaMetricsData} + danger + > + {t('clear')} + </Button> + </Box> + </ModalFooter> + </ModalContent> + </Modal> + ); +} diff --git a/ui/components/app/clear-metametrics-data/index.ts b/ui/components/app/clear-metametrics-data/index.ts new file mode 100644 index 000000000000..b29aee18d564 --- /dev/null +++ b/ui/components/app/clear-metametrics-data/index.ts @@ -0,0 +1 @@ +export { default } from './clear-metametrics-data'; diff --git a/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx new file mode 100644 index 000000000000..cbb541f5648e --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../store/store'; +import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { DATA_DELETION_ERROR_MODAL_CLOSE } from '../../../store/actionConstants'; + +import DataDeletionErrorModal from './data-deletion-error-modal'; + +const mockCloseDeleteMetaMetricsErrorModal = jest + .fn() + .mockImplementation(() => { + return { + type: DATA_DELETION_ERROR_MODAL_CLOSE, + }; + }); + +jest.mock('../../../ducks/app/app.ts', () => { + return { + hideDataDeletionErrorModal: () => { + return mockCloseDeleteMetaMetricsErrorModal(); + }, + }; +}); + +describe('DataDeletionErrorModal', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should render data deletion error modal', async () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<DataDeletionErrorModal />, store); + + expect( + getByText('We are unable to delete this data right now'), + ).toBeInTheDocument(); + expect( + getByText( + "This request can't be completed right now due to an analytics system server issue, please try again later", + ), + ).toBeInTheDocument(); + }); + + it('should call hideDeleteMetaMetricsDataModal when Ok button is clicked', () => { + const store = configureStore({}); + const { getByText } = renderWithProvider(<DataDeletionErrorModal />, store); + expect(getByText('Ok')).toBeEnabled(); + fireEvent.click(getByText('Ok')); + expect(mockCloseDeleteMetaMetricsErrorModal).toHaveBeenCalledTimes(1); + }); +}); diff --git a/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx new file mode 100644 index 000000000000..0b6be4fa782b --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/data-deletion-error-modal.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + Display, + FlexDirection, + AlignItems, + JustifyContent, + TextVariant, + BlockSize, + IconColor, + TextAlign, +} from '../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + ModalOverlay, + ModalContent, + ModalHeader, + Modal, + Box, + Text, + ModalFooter, + Button, + IconName, + ButtonVariant, + Icon, + IconSize, + ButtonSize, +} from '../../component-library'; +import { hideDataDeletionErrorModal } from '../../../ducks/app/app'; + +export default function DataDeletionErrorModal() { + const t = useI18nContext(); + const dispatch = useDispatch(); + + function closeModal() { + dispatch(hideDataDeletionErrorModal()); + } + + return ( + <Modal onClose={closeModal} isOpen> + <ModalOverlay /> + <ModalContent + modalDialogProps={{ + display: Display.Flex, + flexDirection: FlexDirection.Column, + }} + > + <ModalHeader + paddingBottom={4} + paddingRight={6} + paddingLeft={6} + onClose={closeModal} + > + <Box + display={Display.Flex} + flexDirection={FlexDirection.Column} + alignItems={AlignItems.center} + justifyContent={JustifyContent.center} + gap={4} + > + <Icon + size={IconSize.Xl} + name={IconName.Danger} + color={IconColor.warningDefault} + /> + <Text variant={TextVariant.headingSm} textAlign={TextAlign.Center}> + {t('deleteMetaMetricsDataErrorTitle')} + </Text> + </Box> + </ModalHeader> + + <Box + paddingLeft={6} + paddingRight={6} + display={Display.Flex} + gap={4} + flexDirection={FlexDirection.Column} + > + <Text variant={TextVariant.bodySm} textAlign={TextAlign.Justify}> + {t('deleteMetaMetricsDataErrorDesc')} + </Text> + </Box> + + <ModalFooter> + <Box display={Display.Flex} gap={4}> + <Button + size={ButtonSize.Lg} + width={BlockSize.Full} + variant={ButtonVariant.Primary} + onClick={closeModal} + > + {t('ok')} + </Button> + </Box> + </ModalFooter> + </ModalContent> + </Modal> + ); +} diff --git a/ui/components/app/data-deletion-error-modal/index.ts b/ui/components/app/data-deletion-error-modal/index.ts new file mode 100644 index 000000000000..383efd7029b5 --- /dev/null +++ b/ui/components/app/data-deletion-error-modal/index.ts @@ -0,0 +1 @@ +export { default } from './data-deletion-error-modal'; diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 0d6441454f90..9a7a93ea958b 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -301,4 +301,42 @@ describe('App State', () => { }); expect(state.smartTransactionsError).toStrictEqual('Server Side Error'); }); + it('shows delete metametrics modal', () => { + const state = reduceApp(metamaskState, { + type: actions.DELETE_METAMETRICS_DATA_MODAL_OPEN, + }); + + expect(state.showDeleteMetaMetricsDataModal).toStrictEqual(true); + }); + it('hides delete metametrics modal', () => { + const deleteMetaMetricsDataModalState = { + showDeleteMetaMetricsDataModal: true, + }; + const oldState = { ...metamaskState, ...deleteMetaMetricsDataModalState }; + + const state = reduceApp(oldState, { + type: actions.DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }); + + expect(state.showDeleteMetaMetricsDataModal).toStrictEqual(false); + }); + it('shows delete metametrics error modal', () => { + const state = reduceApp(metamaskState, { + type: actions.DATA_DELETION_ERROR_MODAL_OPEN, + }); + + expect(state.showDataDeletionErrorModal).toStrictEqual(true); + }); + it('hides delete metametrics error modal', () => { + const deleteMetaMetricsErrorModalState = { + showDataDeletionErrorModal: true, + }; + const oldState = { ...metamaskState, ...deleteMetaMetricsErrorModalState }; + + const state = reduceApp(oldState, { + type: actions.DATA_DELETION_ERROR_MODAL_CLOSE, + }); + + expect(state.showDataDeletionErrorModal).toStrictEqual(false); + }); }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index 182ba426a3d7..e6a7855ce7a5 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -100,6 +100,8 @@ type AppState = { customTokenAmount: string; txId: string | null; accountDetailsAddress: string; + showDeleteMetaMetricsDataModal: boolean; + showDataDeletionErrorModal: boolean; snapsInstallPrivacyWarningShown: boolean; isAddingNewNetwork: boolean; isMultiRpcOnboarding: boolean; @@ -185,6 +187,8 @@ const initialState: AppState = { scrollToBottom: true, txId: null, accountDetailsAddress: '', + showDeleteMetaMetricsDataModal: false, + showDataDeletionErrorModal: false, snapsInstallPrivacyWarningShown: false, isAddingNewNetwork: false, isMultiRpcOnboarding: false, @@ -608,6 +612,26 @@ export default function reduceApp( isAddingNewNetwork: Boolean(action.payload?.isAddingNewNetwork), isMultiRpcOnboarding: Boolean(action.payload?.isMultiRpcOnboarding), }; + case actionConstants.DELETE_METAMETRICS_DATA_MODAL_OPEN: + return { + ...appState, + showDeleteMetaMetricsDataModal: true, + }; + case actionConstants.DELETE_METAMETRICS_DATA_MODAL_CLOSE: + return { + ...appState, + showDeleteMetaMetricsDataModal: false, + }; + case actionConstants.DATA_DELETION_ERROR_MODAL_OPEN: + return { + ...appState, + showDataDeletionErrorModal: true, + }; + case actionConstants.DATA_DELETION_ERROR_MODAL_CLOSE: + return { + ...appState, + showDataDeletionErrorModal: false, + }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { @@ -717,3 +741,27 @@ export function getLedgerWebHidConnectedStatus( export function getLedgerTransportStatus(state: AppSliceState): string | null { return state.appState.ledgerTransportStatus; } + +export function openDeleteMetaMetricsDataModal(): Action { + return { + type: actionConstants.DELETE_METAMETRICS_DATA_MODAL_OPEN, + }; +} + +export function hideDeleteMetaMetricsDataModal(): Action { + return { + type: actionConstants.DELETE_METAMETRICS_DATA_MODAL_CLOSE, + }; +} + +export function openDataDeletionErrorModal(): Action { + return { + type: actionConstants.DATA_DELETION_ERROR_MODAL_OPEN, + }; +} + +export function hideDataDeletionErrorModal(): Action { + return { + type: actionConstants.DATA_DELETION_ERROR_MODAL_CLOSE, + }; +} diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index 569999f8900e..89cca83f27cf 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -324,6 +324,13 @@ const SETTINGS_CONSTANTS = [ route: `${SECURITY_ROUTE}#dataCollectionForMarketing`, icon: 'fa fa-lock', }, + { + tabMessage: (t) => t('securityAndPrivacy'), + sectionMessage: (t) => t('deleteMetaMetricsData'), + descriptionMessage: (t) => t('deleteMetaMetricsDataDescription'), + route: `${SECURITY_ROUTE}#delete-metametrics-data`, + icon: 'fa fa-lock', + }, { tabMessage: (t) => t('alerts'), sectionMessage: (t) => t('alertSettingsUnconnectedAccount'), diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index bb1637ec2cef..c3d07073a7d3 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -174,7 +174,7 @@ describe('Settings Search Utils', () => { it('returns "Security & privacy" section count', () => { expect( getNumberOfSettingRoutesInTab(t, t('securityAndPrivacy')), - ).toStrictEqual(20); + ).toStrictEqual(21); }); it('returns "Alerts" section count', () => { diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index d18da9cc2eca..343a7f05ecb4 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -1561,6 +1561,59 @@ exports[`Security Tab should match snapshot 1`] = ` </label> </div> </div> + <div + class="mm-box settings-page__content-row mm-box--display-flex mm-box--gap-4 mm-box--flex-direction-column" + data-testid="delete-metametrics-data-button" + > + <div + class="settings-page__content-item" + > + <span> + Delete MetaMetrics data + </span> + <div + class="settings-page__content-description" + > + <span> + + This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our + <a + href="https://consensys.io/privacy-policy/" + rel="noopener noreferrer" + target="_blank" + > + Privacy policy + </a> + . + + </span> + </div> + </div> + <div + class="settings-page__content-item-col" + > + <div + class="mm-box mm-box--display-inline-flex" + > + <span + class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit" + style="mask-image: url('./images/icons/info.svg');" + /> + <p + class="mm-box mm-text mm-text--body-xs mm-box--margin-bottom-2 mm-box--margin-left-1 mm-box--color-text-default" + > + Since you've never opted into MetaMetrics, there's no data to delete here. + </p> + </div> + <button + class="mm-box mm-text mm-button-base mm-button-base--size-md mm-button-base--disabled settings-page__button mm-button-primary mm-button-primary--disabled mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-inverse mm-box--background-color-primary-default mm-box--rounded-pill" + data-theme="light" + disabled="" + > + Delete MetaMetrics data + </button> + </div> + </div> </div> </div> </div> diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx new file mode 100644 index 000000000000..27132fb82f5c --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.test.tsx @@ -0,0 +1,212 @@ +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fireEvent } from '@testing-library/react'; +import configureStore from '../../../../store/store'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; + +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DeleteMetaMetricsDataButton from './delete-metametrics-data-button'; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), + useDispatch: jest.fn(), +})); + +describe('DeleteMetaMetricsDataButton', () => { + const useSelectorMock = useSelector as jest.Mock; + const useDispatchMock = useDispatch as jest.Mock; + const mockDispatch = jest.fn(); + + beforeEach(() => { + useDispatchMock.mockReturnValue(mockDispatch); + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return undefined; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return ''; + } + + return undefined; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const store = configureStore({}); + const { getByTestId, getAllByText, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect(getByTestId('delete-metametrics-data-button')).toBeInTheDocument(); + expect(getAllByText('Delete MetaMetrics data')).toHaveLength(2); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when metrics is opted in and metametrics id is available ', async () => { + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + it('should enable the data deletion button when page mounts after a deletion task is performed and more data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeEnabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + }); + + // if user does not opt in to participate in metrics or for profile sync, metametricsId will not be created. + it('should disable the data deletion button when there is metametrics id not available', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return null; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" This will delete historical MetaMetrics data associated with your use on this device. Your wallet and accounts will remain exactly as they are now after this data has been deleted. This process may take up to 30 days. View our Privacy policy. "`, + ); + expect( + container.querySelector('.settings-page__content-item-col')?.textContent, + ).toMatchInlineSnapshot( + `"Since you've never opted into MetaMetrics, there's no data to delete here.Delete MetaMetrics data"`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + // particilapteInMetrics will be false before the deletion is performed, this way no further data will be recorded after deletion. + it('should disable the data deletion button after a deletion task is performed and no data is recoded after the deletion', async () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getMetaMetricsId) { + return 'fake-metrics-id'; + } + if (selector === getMetaMetricsDataDeletionStatus) { + return 'INITIALIZED'; + } + if (selector === getMetaMetricsDataDeletionTimestamp) { + return 1717779342113; + } + if (selector === getLatestMetricsEventTimestamp) { + return 1717779342110; + } + return undefined; + }); + const store = configureStore({}); + const { getByRole, container } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + expect( + getByRole('button', { name: 'Delete MetaMetrics data' }), + ).toBeDisabled(); + expect( + container.querySelector('.settings-page__content-description') + ?.textContent, + ).toMatchInlineSnapshot( + `" You initiated this action on 7/06/2024. This process can take up to 30 days. View the Privacy policy "`, + ); + }); + + it('should open the modal on the button click', () => { + const store = configureStore({}); + const { getByRole } = renderWithProvider( + <DeleteMetaMetricsDataButton />, + store, + ); + const deleteButton = getByRole('button', { + name: 'Delete MetaMetrics data', + }); + fireEvent.click(deleteButton); + expect(mockDispatch).toHaveBeenCalledWith(openDeleteMetaMetricsDataModal()); + }); +}); diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx new file mode 100644 index 000000000000..34b61697ed95 --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/delete-metametrics-data-button.tsx @@ -0,0 +1,147 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CONSENSYS_PRIVACY_LINK } from '../../../../../shared/lib/ui-utils'; +import ClearMetametricsData from '../../../../components/app/clear-metametrics-data'; +import { + Box, + ButtonPrimary, + Icon, + IconName, + IconSize, + PolymorphicComponentPropWithRef, + PolymorphicRef, + Text, +} from '../../../../components/component-library'; +import { + Display, + FlexDirection, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + getMetaMetricsDataDeletionTimestamp, + getMetaMetricsDataDeletionStatus, + getMetaMetricsId, + getShowDataDeletionErrorModal, + getShowDeleteMetaMetricsDataModal, + getLatestMetricsEventTimestamp, +} from '../../../../selectors'; +import { openDeleteMetaMetricsDataModal } from '../../../../ducks/app/app'; +import DataDeletionErrorModal from '../../../../components/app/data-deletion-error-modal'; +import { formatDate } from '../../../../helpers/utils/util'; +import { DeleteRegulationStatus } from '../../../../../shared/constants/metametrics'; + +type DeleteMetaMetricsDataButtonProps<C extends React.ElementType> = + PolymorphicComponentPropWithRef<C>; + +type DeleteMetaMetricsDataButtonComponent = < + C extends React.ElementType = 'div', +>( + props: DeleteMetaMetricsDataButtonProps<C>, +) => React.ReactElement | null; + +const DeleteMetaMetricsDataButton: DeleteMetaMetricsDataButtonComponent = + React.forwardRef( + <C extends React.ElementType = 'div'>( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { ...props }: DeleteMetaMetricsDataButtonProps<C>, + ref: PolymorphicRef<C>, + ) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + + const metaMetricsId = useSelector(getMetaMetricsId); + const metaMetricsDataDeletionStatus: DeleteRegulationStatus = useSelector( + getMetaMetricsDataDeletionStatus, + ); + const metaMetricsDataDeletionTimestamp = useSelector( + getMetaMetricsDataDeletionTimestamp, + ); + const formatedDate = formatDate( + metaMetricsDataDeletionTimestamp, + 'd/MM/y', + ); + + const showDeleteMetaMetricsDataModal = useSelector( + getShowDeleteMetaMetricsDataModal, + ); + const showDataDeletionErrorModal = useSelector( + getShowDataDeletionErrorModal, + ); + const latestMetricsEventTimestamp = useSelector( + getLatestMetricsEventTimestamp, + ); + + let dataDeletionButtonDisabled = Boolean(!metaMetricsId); + if (!dataDeletionButtonDisabled && metaMetricsDataDeletionStatus) { + dataDeletionButtonDisabled = + [ + DeleteRegulationStatus.Initialized, + DeleteRegulationStatus.Running, + DeleteRegulationStatus.Finished, + ].includes(metaMetricsDataDeletionStatus) && + metaMetricsDataDeletionTimestamp > latestMetricsEventTimestamp; + } + const privacyPolicyLink = ( + <a + href={CONSENSYS_PRIVACY_LINK} + target="_blank" + rel="noopener noreferrer" + key="metametrics-consensys-privacy-link" + > + {t('privacyMsg')} + </a> + ); + return ( + <> + <Box + ref={ref} + className="settings-page__content-row" + data-testid="delete-metametrics-data-button" + display={Display.Flex} + flexDirection={FlexDirection.Column} + gap={4} + > + <div className="settings-page__content-item"> + <span>{t('deleteMetaMetricsData')}</span> + <div className="settings-page__content-description"> + {dataDeletionButtonDisabled && Boolean(metaMetricsId) + ? t('deleteMetaMetricsDataRequestedDescription', [ + formatedDate, + privacyPolicyLink, + ]) + : t('deleteMetaMetricsDataDescription', [privacyPolicyLink])} + </div> + </div> + <div className="settings-page__content-item-col"> + {Boolean(!metaMetricsId) && ( + <Box display={Display.InlineFlex}> + <Icon name={IconName.Info} size={IconSize.Sm} /> + <Text + variant={TextVariant.bodyXs} + marginLeft={1} + marginBottom={2} + > + {t('metaMetricsIdNotAvailableError')} + </Text> + </Box> + )} + <ButtonPrimary + className="settings-page__button" + onClick={() => { + dispatch(openDeleteMetaMetricsDataModal()); + }} + disabled={dataDeletionButtonDisabled} + > + {t('deleteMetaMetricsData')} + </ButtonPrimary> + </div> + </Box> + {showDeleteMetaMetricsDataModal && <ClearMetametricsData />} + {showDataDeletionErrorModal && <DataDeletionErrorModal />} + </> + ); + }, + ); + +export default DeleteMetaMetricsDataButton; diff --git a/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts new file mode 100644 index 000000000000..945f4d349ede --- /dev/null +++ b/ui/pages/settings/security-tab/delete-metametrics-data-button/index.ts @@ -0,0 +1 @@ +export { default } from './delete-metametrics-data-button'; diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index f6da9fe2367f..1fae729d3f31 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -52,8 +52,10 @@ import { } from '../../../helpers/utils/settings-search'; import IncomingTransactionToggle from '../../../components/app/incoming-trasaction-toggle/incoming-transaction-toggle'; -import ProfileSyncToggle from './profile-sync-toggle'; +import { updateDataDeletionTaskStatus } from '../../../store/actions'; import MetametricsToggle from './metametrics-toggle'; +import ProfileSyncToggle from './profile-sync-toggle'; +import DeleteMetametricsDataButton from './delete-metametrics-data-button'; export default class SecurityTab extends PureComponent { static contextTypes = { @@ -102,6 +104,7 @@ export default class SecurityTab extends PureComponent { useExternalServices: PropTypes.bool, toggleExternalServices: PropTypes.func.isRequired, setSecurityAlertsEnabled: PropTypes.func, + metaMetricsDataDeletionId: PropTypes.string, }; state = { @@ -138,9 +141,12 @@ export default class SecurityTab extends PureComponent { } } - componentDidMount() { + async componentDidMount() { const { t } = this.context; handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs); + if (this.props.metaMetricsDataDeletionId) { + await updateDataDeletionTaskStatus(); + } } toggleSetting(value, eventName, eventAction, toggleMethod) { @@ -961,7 +967,7 @@ export default class SecurityTab extends PureComponent { return ( <Box - ref={this.settingsRefs[18]} + ref={this.settingsRefs[17]} className="settings-page__content-row" display={Display.Flex} flexDirection={FlexDirection.Row} @@ -1222,6 +1228,7 @@ export default class SecurityTab extends PureComponent { setDataCollectionForMarketing={setDataCollectionForMarketing} /> {this.renderDataCollectionForMarketing()} + <DeleteMetametricsDataButton ref={this.settingsRefs[20]} /> </div> </div> ); diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index 747e3738fe3f..224072ef2b10 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -20,10 +20,12 @@ import { setUseExternalNameSources, setUseTransactionSimulations, setSecurityAlertsEnabled, + updateDataDeletionTaskStatus, } from '../../../store/actions'; import { getIsSecurityAlertsEnabled, getNetworkConfigurationsByChainId, + getMetaMetricsDataDeletionId, getPetnamesEnabled, } from '../../../selectors'; import { openBasicFunctionalityModal } from '../../../ducks/app/app'; @@ -78,6 +80,7 @@ const mapStateToProps = (state) => { petnamesEnabled, securityAlertsEnabled: getIsSecurityAlertsEnabled(state), useTransactionSimulations: metamask.useTransactionSimulations, + metaMetricsDataDeletionId: getMetaMetricsDataDeletionId(state), }; }; @@ -116,6 +119,9 @@ const mapDispatchToProps = (dispatch) => { setUseTransactionSimulations: (value) => { return dispatch(setUseTransactionSimulations(value)); }, + updateDataDeletionTaskStatus: () => { + return updateDataDeletionTaskStatus(); + }, setSecurityAlertsEnabled: (value) => setSecurityAlertsEnabled(value), }; }; diff --git a/ui/pages/settings/security-tab/security-tab.test.js b/ui/pages/settings/security-tab/security-tab.test.js index 905fec684fd5..5e31cfb68c57 100644 --- a/ui/pages/settings/security-tab/security-tab.test.js +++ b/ui/pages/settings/security-tab/security-tab.test.js @@ -13,6 +13,8 @@ import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { getIsSecurityAlertsEnabled } from '../../../selectors'; import SecurityTab from './security-tab.container'; +const mockOpenDeleteMetaMetricsDataModal = jest.fn(); + const mockSetSecurityAlertsEnabled = jest .fn() .mockImplementation(() => () => undefined); @@ -36,6 +38,14 @@ jest.mock('../../../store/actions', () => ({ setSecurityAlertsEnabled: (val) => mockSetSecurityAlertsEnabled(val), })); +jest.mock('../../../ducks/app/app.ts', () => { + return { + openDeleteMetaMetricsDataModal: () => { + return mockOpenDeleteMetaMetricsDataModal; + }, + }; +}); + describe('Security Tab', () => { mockState.appState.warning = 'warning'; // This tests an otherwise untested render branch @@ -214,7 +224,23 @@ describe('Security Tab', () => { await user.click(screen.getByText(tEn('addCustomNetwork'))); expect(global.platform.openExtensionInBrowser).toHaveBeenCalled(); }); + it('clicks "Delete MetaMetrics Data"', async () => { + mockState.metamask.participateInMetaMetrics = true; + mockState.metamask.metaMetricsId = 'fake-metametrics-id'; + const localMockStore = configureMockStore([thunk])(mockState); + renderWithProvider(<SecurityTab />, localMockStore); + + expect( + screen.queryByTestId(`delete-metametrics-data-button`), + ).toBeInTheDocument(); + + fireEvent.click( + screen.getByRole('button', { name: 'Delete MetaMetrics data' }), + ); + + expect(mockOpenDeleteMetaMetricsDataModal).toHaveBeenCalled(); + }); describe('Blockaid', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/ui/selectors/metametrics.js b/ui/selectors/metametrics.js index c623e378c003..1b0a9dd603dd 100644 --- a/ui/selectors/metametrics.js +++ b/ui/selectors/metametrics.js @@ -8,6 +8,9 @@ export const getDataCollectionForMarketing = (state) => export const getParticipateInMetaMetrics = (state) => Boolean(state.metamask.participateInMetaMetrics); +export const getLatestMetricsEventTimestamp = (state) => + state.metamask.latestNonAnonymousEventTimestamp; + export const selectFragmentBySuccessEvent = createSelector( selectFragments, (_, fragmentOptions) => fragmentOptions, diff --git a/ui/selectors/metametrics.test.js b/ui/selectors/metametrics.test.js index 13185a47700b..454def7d92a4 100644 --- a/ui/selectors/metametrics.test.js +++ b/ui/selectors/metametrics.test.js @@ -2,6 +2,7 @@ const { selectFragmentBySuccessEvent, selectFragmentById, selectMatchingFragment, + getLatestMetricsEventTimestamp, } = require('.'); describe('selectFragmentBySuccessEvent', () => { @@ -68,4 +69,15 @@ describe('selectMatchingFragment', () => { }); expect(selected).toHaveProperty('id', 'randomid'); }); + describe('getLatestMetricsEventTimestamp', () => { + it('should find matching fragment in state by id', () => { + const state = { + metamask: { + latestNonAnonymousEventTimestamp: 12345, + }, + }; + const timestamp = getLatestMetricsEventTimestamp(state); + expect(timestamp).toBe(12345); + }); + }); }); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index fac2f9f52c31..644924a41e3e 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2561,6 +2561,26 @@ export function getNameSources(state) { return state.metamask.nameSources || {}; } +export function getShowDeleteMetaMetricsDataModal(state) { + return state.appState.showDeleteMetaMetricsDataModal; +} + +export function getShowDataDeletionErrorModal(state) { + return state.appState.showDataDeletionErrorModal; +} + +export function getMetaMetricsDataDeletionId(state) { + return state.metamask.metaMetricsDataDeletionId; +} + +export function getMetaMetricsDataDeletionTimestamp(state) { + return state.metamask.metaMetricsDataDeletionTimestamp; +} + +export function getMetaMetricsDataDeletionStatus(state) { + return state.metamask.metaMetricsDataDeletionStatus; +} + /** * To get all installed snaps with proper metadata * diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 342e7d7187c8..24b2a2afe125 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -11,6 +11,7 @@ import { createMockInternalAccount } from '../../test/jest/mocks'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { getProviderConfig } from '../ducks/metamask/metamask'; import { mockNetworkState } from '../../test/stub/networks'; +import { DeleteRegulationStatus } from '../../shared/constants/metametrics'; import * as selectors from './selectors'; jest.mock('../../app/scripts/lib/util', () => ({ @@ -2018,4 +2019,65 @@ describe('#getConnectedSitesList', () => { }, }); }); + describe('#getShowDeleteMetaMetricsDataModal', () => { + it('returns state of showDeleteMetaMetricsDataModal', () => { + expect( + selectors.getShowDeleteMetaMetricsDataModal({ + appState: { + showDeleteMetaMetricsDataModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getShowDataDeletionErrorModal', () => { + it('returns state of showDataDeletionErrorModal', () => { + expect( + selectors.getShowDataDeletionErrorModal({ + appState: { + showDataDeletionErrorModal: true, + }, + }), + ).toStrictEqual(true); + }); + }); + describe('#getMetaMetricsDataDeletionId', () => { + it('returns metaMetricsDataDeletionId', () => { + expect( + selectors.getMetaMetricsDataDeletionId({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123'); + }); + }); + describe('#getMetaMetricsDataDeletionTimestamp', () => { + it('returns metaMetricsDataDeletionTimestamp', () => { + expect( + selectors.getMetaMetricsDataDeletionTimestamp({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('123345'); + }); + }); + describe('#getMetaMetricsDataDeletionStatus', () => { + it('returns metaMetricsDataDeletionStatus', () => { + expect( + selectors.getMetaMetricsDataDeletionStatus({ + metamask: { + metaMetricsDataDeletionId: '123', + metaMetricsDataDeletionTimestamp: '123345', + metaMetricsDataDeletionStatus: DeleteRegulationStatus.Initialized, + }, + }), + ).toStrictEqual('INITIALIZED'); + }); + }); }); diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 074568cfbf1d..6e1e33d9531f 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -99,6 +99,14 @@ export const UPDATE_CUSTOM_NONCE = 'UPDATE_CUSTOM_NONCE'; export const SET_PARTICIPATE_IN_METAMETRICS = 'SET_PARTICIPATE_IN_METAMETRICS'; export const SET_DATA_COLLECTION_FOR_MARKETING = 'SET_DATA_COLLECTION_FOR_MARKETING'; +export const DELETE_METAMETRICS_DATA_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_MODAL_OPEN'; +export const DELETE_METAMETRICS_DATA_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_MODAL_CLOSE'; +export const DATA_DELETION_ERROR_MODAL_OPEN = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_OPEN'; +export const DATA_DELETION_ERROR_MODAL_CLOSE = + 'DELETE_METAMETRICS_DATA_ERROR_MODAL_CLOSE'; // locale export const SET_CURRENT_LOCALE = 'SET_CURRENT_LOCALE'; From fd4cdf0826dd4e99b10c1c7df2a528545938d23b Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:04:01 +0200 Subject: [PATCH 10/12] fix: Test coverage quality gate (#27581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27581?quickstart=1) Fixes test coverage quality gates. ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3328 ## **Manual testing steps** 1. Test coverage should be correctly reported/validated ## **Screenshots/Recordings** Not applicable ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .github/workflows/main.yml | 26 +++++++++- .github/workflows/run-tests.yml | 68 ++++++++++++--------------- .github/workflows/sonarcloud.yml | 30 ++++++++++++ .github/workflows/update-coverage.yml | 48 +++++++++++++++++++ coverage.json | 1 + 5 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/sonarcloud.yml create mode 100644 .github/workflows/update-coverage.yml create mode 100644 coverage.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d14fefe82717..5d1b4d73bdab 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,8 +2,15 @@ name: Main on: push: - branches: [develop, master] + branches: + - develop + - master pull_request: + types: + - opened + - reopened + - synchronize + merge_group: jobs: check-workflows: @@ -21,11 +28,25 @@ jobs: run: ${{ steps.download-actionlint.outputs.executable }} -color shell: bash + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + + sonarcloud: + name: SonarCloud + uses: ./.github/workflows/sonarcloud.yml + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + needs: + - run-tests + all-jobs-completed: name: All jobs completed runs-on: ubuntu-latest needs: - check-workflows + - run-tests + - sonarcloud outputs: PASSED: ${{ steps.set-output.outputs.PASSED }} steps: @@ -37,7 +58,8 @@ jobs: name: All jobs pass if: ${{ always() }} runs-on: ubuntu-latest - needs: all-jobs-completed + needs: + - all-jobs-completed steps: - name: Check that all jobs have passed run: | diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 77958f69da2d..3cb7c50e573a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,16 +1,14 @@ name: Run tests on: - push: - branches: - - develop - - master - pull_request: - types: - - opened - - reopened - - synchronize - merge_group: + workflow_call: + outputs: + current-coverage: + description: Current coverage + value: ${{ jobs.report-coverage.outputs.current-coverage }} + stored-coverage: + description: Stored coverage + value: ${{ jobs.report-coverage.outputs.stored-coverage }} jobs: test-unit: @@ -79,18 +77,19 @@ jobs: name: coverage-integration path: coverage/integration/coverage-integration.json - sonarcloud: - name: SonarCloud + report-coverage: + name: Report coverage runs-on: ubuntu-latest needs: - test-unit - test-webpack - test-integration + outputs: + current-coverage: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + stored-coverage: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} steps: - name: Checkout repository uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - name: Setup environment uses: metamask/github-tools/.github/actions/setup-environment@main @@ -109,35 +108,28 @@ jobs: name: lcov.info path: coverage/lcov.info - - name: Get Sonar coverage - id: get-sonar-coverage - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Get current coverage + id: get-current-coverage + run: | + current_coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print int($3)}') + echo "The current coverage is $current_coverage%." + echo 'CURRENT_COVERAGE='"$current_coverage" >> "$GITHUB_OUTPUT" + + - name: Get stored coverage + id: get-stored-coverage run: | - projectKey=$(grep 'sonar.projectKey=' sonar-project.properties | cut -d'=' -f2) - sonar_coverage=$(curl --silent --header "Authorization: Bearer $SONAR_TOKEN" "https://sonarcloud.io/api/measures/component?component=$projectKey&metricKeys=coverage" | jq -r '.component.measures[0].value // 0') - echo "The Sonar coverage of $projectKey is $sonar_coverage%." - echo 'SONAR_COVERAGE='"$sonar_coverage" >> "$GITHUB_OUTPUT" + stored_coverage=$(jq ".coverage" coverage.json) + echo "The stored coverage is $stored_coverage%." + echo 'STORED_COVERAGE='"$stored_coverage" >> "$GITHUB_OUTPUT" - name: Validate test coverage env: - SONAR_COVERAGE: ${{ steps.get-sonar-coverage.outputs.SONAR_COVERAGE }} + CURRENT_COVERAGE: ${{ steps.get-current-coverage.outputs.CURRENT_COVERAGE }} + STORED_COVERAGE: ${{ steps.get-stored-coverage.outputs.STORED_COVERAGE }} run: | - coverage=$(yarn nyc report --reporter=text-summary | grep 'Lines' | awk '{gsub(/%/, ""); print $3}') - if [ -z "$coverage" ]; then - echo "::error::Could not retrieve test coverage." - exit 1 - fi - if (( $(echo "$coverage < $SONAR_COVERAGE" | bc -l) )); then - echo "::error::Quality gate failed for test coverage. Current test coverage is $coverage%, please increase coverage to at least $SONAR_COVERAGE%." + if (( $(echo "$CURRENT_COVERAGE < $STORED_COVERAGE" | bc -l) )); then + echo "::error::Quality gate failed for test coverage. Current coverage is $CURRENT_COVERAGE%, please increase coverage to at least $STORED_COVERAGE%." exit 1 else - echo "Test coverage is $coverage%. Quality gate passed." + echo "The current coverage is $CURRENT_COVERAGE%, stored coverage is $STORED_COVERAGE%. Quality gate passed." fi - - - name: SonarCloud Scan - # This is SonarSource/sonarcloud-github-action@v2.0.0 - uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 000000000000..460d5c140462 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,30 @@ +name: SonarCloud + +on: + workflow_call: + secrets: + SONAR_TOKEN: + required: true + +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: lcov.info + path: coverage + + - name: SonarCloud Scan + # This is SonarSource/sonarcloud-github-action@v2.0.0 + uses: SonarSource/sonarcloud-github-action@4b4d7634dab97dcee0b75763a54a6dc92a9e6bc1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/update-coverage.yml b/.github/workflows/update-coverage.yml new file mode 100644 index 000000000000..f246bde7eb32 --- /dev/null +++ b/.github/workflows/update-coverage.yml @@ -0,0 +1,48 @@ +name: Update coverage + +on: + schedule: + # Once per day at midnight UTC + - cron: 0 0 * * * + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + run-tests: + name: Run tests + uses: ./.github/workflows/run-tests.yml + + update-coverage: + if: ${{ needs.run-tests.outputs.current-coverage > needs.run-tests.outputs.stored-coverage }} + name: Update coverage + runs-on: ubuntu-latest + needs: + - run-tests + env: + CURRENT_COVERAGE: ${{ needs.run-tests.outputs.current-coverage }} + STORED_COVERAGE: ${{ needs.run-tests.outputs.stored-coverage }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Update coverage + run: | + echo "{ \"coverage\": $CURRENT_COVERAGE }" > coverage.json + + - name: Checkout/create branch, commit, and force push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b metamaskbot/update-coverage + git add coverage.json + git commit -m "chore: Update coverage.json" + git push -f origin metamaskbot/update-coverage + + - name: Create/update pull request + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create --title "chore: Update coverage.json" --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." --base develop --head metamaskbot/update-coverage || gh pr edit --body "This PR is automatically opened to update the coverage.json file when test coverage increases. Coverage increased from $STORED_COVERAGE% to $CURRENT_COVERAGE%." diff --git a/coverage.json b/coverage.json new file mode 100644 index 000000000000..f65ea343e9b3 --- /dev/null +++ b/coverage.json @@ -0,0 +1 @@ +{ "coverage": 0 } From bff1e2160746363085f1f5a9bb92eb5e0e958554 Mon Sep 17 00:00:00 2001 From: Howard Braham <howrad@gmail.com> Date: Mon, 7 Oct 2024 20:48:14 -0700 Subject: [PATCH 11/12] refactor: routes constants (#27078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This is one small step in the larger task to refactor routing, in order to speed up load time (MetaMask/MetaMask-planning#2898) The changes are mostly to increase DRY, and to make a closer coupling between connected routes and their analytics tracking names. I wanted to get this in separately in order to reduce complexity and merge conflicts later. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27078?quickstart=1) ## **Related issues** Progresses: MetaMask/MetaMask-planning#2898 ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> --- ui/helpers/constants/routes.ts | 583 ++++++++++++++++----------------- 1 file changed, 284 insertions(+), 299 deletions(-) diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index c755c9914f25..eec9075a64d8 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -1,307 +1,292 @@ -const DEFAULT_ROUTE = '/'; -const UNLOCK_ROUTE = '/unlock'; -const LOCK_ROUTE = '/lock'; -const ASSET_ROUTE = '/asset'; -const SETTINGS_ROUTE = '/settings'; -const GENERAL_ROUTE = '/settings/general'; -const ADVANCED_ROUTE = '/settings/advanced'; - -const DEVELOPER_OPTIONS_ROUTE = '/settings/developer-options'; -const EXPERIMENTAL_ROUTE = '/settings/experimental'; -const SECURITY_ROUTE = '/settings/security'; -const ABOUT_US_ROUTE = '/settings/about-us'; -const ALERTS_ROUTE = '/settings/alerts'; -const NETWORKS_ROUTE = '/settings/networks'; -const NETWORKS_FORM_ROUTE = '/settings/networks/form'; -const ADD_NETWORK_ROUTE = '/settings/networks/add-network'; -const ADD_POPULAR_CUSTOM_NETWORK = +// PATH_NAME_MAP is used to pull a convenient name for analytics tracking events. The key must +// be react-router ready path, and can include params such as :id for popup windows +export const PATH_NAME_MAP: { [key: string]: string } = {}; + +export const DEFAULT_ROUTE = '/'; +PATH_NAME_MAP[DEFAULT_ROUTE] = 'Home'; + +export const UNLOCK_ROUTE = '/unlock'; +PATH_NAME_MAP[UNLOCK_ROUTE] = 'Unlock Page'; + +export const LOCK_ROUTE = '/lock'; +PATH_NAME_MAP[LOCK_ROUTE] = 'Lock Page'; + +export const ASSET_ROUTE = '/asset'; +PATH_NAME_MAP[`${ASSET_ROUTE}/:asset/:id`] = `Asset Page`; +PATH_NAME_MAP[`${ASSET_ROUTE}/image/:asset/:id`] = `Nft Image Page`; + +export const SETTINGS_ROUTE = '/settings'; +PATH_NAME_MAP[SETTINGS_ROUTE] = 'Settings Page'; + +export const GENERAL_ROUTE = '/settings/general'; +PATH_NAME_MAP[GENERAL_ROUTE] = 'General Settings Page'; + +export const ADVANCED_ROUTE = '/settings/advanced'; +PATH_NAME_MAP[ADVANCED_ROUTE] = 'Advanced Settings Page'; + +export const DEVELOPER_OPTIONS_ROUTE = '/settings/developer-options'; +// DEVELOPER_OPTIONS_ROUTE not in PATH_NAME_MAP because we're not tracking analytics for this page + +export const EXPERIMENTAL_ROUTE = '/settings/experimental'; +PATH_NAME_MAP[EXPERIMENTAL_ROUTE] = 'Experimental Settings Page'; + +export const SECURITY_ROUTE = '/settings/security'; +PATH_NAME_MAP[SECURITY_ROUTE] = 'Security Settings Page'; + +export const ABOUT_US_ROUTE = '/settings/about-us'; +PATH_NAME_MAP[ABOUT_US_ROUTE] = 'About Us Page'; + +export const ALERTS_ROUTE = '/settings/alerts'; +PATH_NAME_MAP[ALERTS_ROUTE] = 'Alerts Settings Page'; + +export const NETWORKS_ROUTE = '/settings/networks'; +PATH_NAME_MAP[NETWORKS_ROUTE] = 'Network Settings Page'; + +export const NETWORKS_FORM_ROUTE = '/settings/networks/form'; +PATH_NAME_MAP[NETWORKS_FORM_ROUTE] = 'Network Settings Page Form'; + +export const ADD_NETWORK_ROUTE = '/settings/networks/add-network'; +PATH_NAME_MAP[ADD_NETWORK_ROUTE] = 'Add Network From Settings Page Form'; + +export const ADD_POPULAR_CUSTOM_NETWORK = '/settings/networks/add-popular-custom-network'; -const CONTACT_LIST_ROUTE = '/settings/contact-list'; -const CONTACT_EDIT_ROUTE = '/settings/contact-list/edit-contact'; -const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact'; -const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact'; -const REVEAL_SEED_ROUTE = '/seed'; -const RESTORE_VAULT_ROUTE = '/restore-vault'; -const IMPORT_TOKEN_ROUTE = '/import-token'; -const IMPORT_TOKENS_ROUTE = '/import-tokens'; -const CONFIRM_IMPORT_TOKEN_ROUTE = '/confirm-import-token'; -const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'; -const NEW_ACCOUNT_ROUTE = '/new-account'; -const CONFIRM_ADD_SUGGESTED_NFT_ROUTE = '/confirm-add-suggested-nft'; -const CONNECT_HARDWARE_ROUTE = '/new-account/connect'; +PATH_NAME_MAP[ADD_POPULAR_CUSTOM_NETWORK] = + 'Add Network From A List Of Popular Custom Networks'; + +export const CONTACT_LIST_ROUTE = '/settings/contact-list'; +PATH_NAME_MAP[CONTACT_LIST_ROUTE] = 'Contact List Settings Page'; + +export const CONTACT_EDIT_ROUTE = '/settings/contact-list/edit-contact'; +PATH_NAME_MAP[`${CONTACT_EDIT_ROUTE}/:address`] = 'Edit Contact Settings Page'; + +export const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact'; +PATH_NAME_MAP[CONTACT_ADD_ROUTE] = 'Add Contact Settings Page'; + +export const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact'; +PATH_NAME_MAP[`${CONTACT_VIEW_ROUTE}/:address`] = 'View Contact Settings Page'; + +export const REVEAL_SEED_ROUTE = '/seed'; +PATH_NAME_MAP[REVEAL_SEED_ROUTE] = 'Reveal Secret Recovery Phrase Page'; + +export const RESTORE_VAULT_ROUTE = '/restore-vault'; +PATH_NAME_MAP[RESTORE_VAULT_ROUTE] = 'Restore Vault Page'; + +export const IMPORT_TOKEN_ROUTE = '/import-token'; +PATH_NAME_MAP[IMPORT_TOKEN_ROUTE] = 'Import Token Page'; + +export const IMPORT_TOKENS_ROUTE = '/import-tokens'; +PATH_NAME_MAP[IMPORT_TOKENS_ROUTE] = 'Import Tokens Page'; + +export const CONFIRM_IMPORT_TOKEN_ROUTE = '/confirm-import-token'; +PATH_NAME_MAP[CONFIRM_IMPORT_TOKEN_ROUTE] = 'Confirm Import Token Page'; + +export const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'; +PATH_NAME_MAP[CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE] = + 'Confirm Add Suggested Token Page'; + +export const NEW_ACCOUNT_ROUTE = '/new-account'; +PATH_NAME_MAP[NEW_ACCOUNT_ROUTE] = 'New Account Page'; + +export const CONFIRM_ADD_SUGGESTED_NFT_ROUTE = '/confirm-add-suggested-nft'; +PATH_NAME_MAP[CONFIRM_ADD_SUGGESTED_NFT_ROUTE] = + 'Confirm Add Suggested NFT Page'; + +export const CONNECT_HARDWARE_ROUTE = '/new-account/connect'; +PATH_NAME_MAP[CONNECT_HARDWARE_ROUTE] = 'Connect Hardware Wallet Page'; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody'; -const INSTITUTIONAL_FEATURES_DONE_ROUTE = '/institutional-features/done'; -const CUSTODY_ACCOUNT_DONE_ROUTE = '/new-account/custody/done'; -const CONFIRM_ADD_CUSTODIAN_TOKEN = '/confirm-add-custodian-token'; -const INTERACTIVE_REPLACEMENT_TOKEN_PAGE = +export const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody'; +PATH_NAME_MAP[CUSTODY_ACCOUNT_ROUTE] = 'Connect Custody'; + +export const INSTITUTIONAL_FEATURES_DONE_ROUTE = '/institutional-features/done'; +PATH_NAME_MAP[INSTITUTIONAL_FEATURES_DONE_ROUTE] = + 'Institutional Features Done Page'; + +export const CUSTODY_ACCOUNT_DONE_ROUTE = '/new-account/custody/done'; +PATH_NAME_MAP[CUSTODY_ACCOUNT_DONE_ROUTE] = 'Connect Custody Account done'; + +export const CONFIRM_ADD_CUSTODIAN_TOKEN = '/confirm-add-custodian-token'; +PATH_NAME_MAP[CONFIRM_ADD_CUSTODIAN_TOKEN] = 'Confirm Add Custodian Token'; + +export const INTERACTIVE_REPLACEMENT_TOKEN_PAGE = '/interactive-replacement-token-page'; -const SRP_REMINDER = '/onboarding/remind-srp'; +PATH_NAME_MAP[INTERACTIVE_REPLACEMENT_TOKEN_PAGE] = + 'Interactive replacement token page'; + +export const SRP_REMINDER = '/onboarding/remind-srp'; +PATH_NAME_MAP[SRP_REMINDER] = 'Secret Recovery Phrase Reminder'; ///: END:ONLY_INCLUDE_IF -const SEND_ROUTE = '/send'; -const CONNECTIONS = '/connections'; -const REVIEW_PERMISSIONS = '/review-permissions'; -const PERMISSIONS = '/permissions'; -const TOKEN_DETAILS = '/token-details'; -const CONNECT_ROUTE = '/connect'; -const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'; -const CONNECT_SNAPS_CONNECT_ROUTE = '/snaps-connect'; -const CONNECT_SNAP_INSTALL_ROUTE = '/snap-install'; -const CONNECT_SNAP_UPDATE_ROUTE = '/snap-update'; -const CONNECT_SNAP_RESULT_ROUTE = '/snap-install-result'; -const SNAPS_ROUTE = '/snaps'; -const SNAPS_VIEW_ROUTE = '/snaps/view'; -const NOTIFICATIONS_ROUTE = '/notifications'; -const NOTIFICATIONS_SETTINGS_ROUTE = '/notifications/settings'; -const CONNECTED_ROUTE = '/connected'; -const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'; -const CROSS_CHAIN_SWAP_ROUTE = '/cross-chain'; -const SWAPS_ROUTE = '/swaps'; -const PREPARE_SWAP_ROUTE = '/swaps/prepare-swap-page'; -const SWAPS_NOTIFICATION_ROUTE = '/swaps/notification-page'; -const BUILD_QUOTE_ROUTE = '/swaps/build-quote'; -const VIEW_QUOTE_ROUTE = '/swaps/view-quote'; -const LOADING_QUOTES_ROUTE = '/swaps/loading-quotes'; -const AWAITING_SIGNATURES_ROUTE = '/swaps/awaiting-signatures'; -const SMART_TRANSACTION_STATUS_ROUTE = '/swaps/smart-transaction-status'; -const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap'; -const SWAPS_ERROR_ROUTE = '/swaps/swaps-error'; -const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance'; - -const ONBOARDING_ROUTE = '/onboarding'; -const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase'; -const ONBOARDING_CONFIRM_SRP_ROUTE = '/onboarding/confirm-recovery-phrase'; -const ONBOARDING_CREATE_PASSWORD_ROUTE = '/onboarding/create-password'; -const ONBOARDING_COMPLETION_ROUTE = '/onboarding/completion'; -const MMI_ONBOARDING_COMPLETION_ROUTE = '/onboarding/account-completion'; -const ONBOARDING_UNLOCK_ROUTE = '/onboarding/unlock'; -const ONBOARDING_HELP_US_IMPROVE_ROUTE = '/onboarding/help-us-improve'; -const ONBOARDING_IMPORT_WITH_SRP_ROUTE = + +export const SEND_ROUTE = '/send'; +PATH_NAME_MAP[SEND_ROUTE] = 'Send Page'; + +export const CONNECTIONS = '/connections'; +PATH_NAME_MAP[CONNECTIONS] = 'Connections'; + +export const PERMISSIONS = '/permissions'; +PATH_NAME_MAP[PERMISSIONS] = 'Permissions'; + +export const REVIEW_PERMISSIONS = '/review-permissions'; + +export const TOKEN_DETAILS = '/token-details'; +PATH_NAME_MAP[`${TOKEN_DETAILS}/:address`] = 'Token Details Page'; + +export const CONNECT_ROUTE = '/connect'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id`] = 'Connect To Site Confirmation Page'; + +export const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_CONFIRM_PERMISSIONS_ROUTE}`] = + 'Grant Connected Site Permissions Confirmation Page'; + +export const CONNECT_SNAPS_CONNECT_ROUTE = '/snaps-connect'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAPS_CONNECT_ROUTE}`] = + 'Snaps Connect Page'; + +export const CONNECT_SNAP_INSTALL_ROUTE = '/snap-install'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAP_INSTALL_ROUTE}`] = + 'Snap Install Page'; + +export const CONNECT_SNAP_UPDATE_ROUTE = '/snap-update'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAP_UPDATE_ROUTE}`] = + 'Snap Update Page'; + +export const CONNECT_SNAP_RESULT_ROUTE = '/snap-install-result'; +PATH_NAME_MAP[`${CONNECT_ROUTE}/:id${CONNECT_SNAP_RESULT_ROUTE}`] = + 'Snap Install Result Page'; + +export const SNAPS_ROUTE = '/snaps'; +PATH_NAME_MAP[SNAPS_ROUTE] = 'Snaps List Page'; + +export const SNAPS_VIEW_ROUTE = '/snaps/view'; +PATH_NAME_MAP[`${SNAPS_VIEW_ROUTE}/:snapId`] = 'Snap View Page'; + +export const NOTIFICATIONS_ROUTE = '/notifications'; +PATH_NAME_MAP[NOTIFICATIONS_ROUTE] = 'Notifications Page'; +PATH_NAME_MAP[`${NOTIFICATIONS_ROUTE}/:uuid`] = 'Notification Detail Page'; + +export const NOTIFICATIONS_SETTINGS_ROUTE = '/notifications/settings'; +PATH_NAME_MAP[NOTIFICATIONS_SETTINGS_ROUTE] = 'Notifications Settings Page'; + +export const CONNECTED_ROUTE = '/connected'; +PATH_NAME_MAP[CONNECTED_ROUTE] = 'Sites Connected To This Account Page'; + +export const CONNECTED_ACCOUNTS_ROUTE = '/connected/accounts'; +PATH_NAME_MAP[CONNECTED_ACCOUNTS_ROUTE] = + 'Accounts Connected To This Site Page'; + +export const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'; +PATH_NAME_MAP[CONFIRM_TRANSACTION_ROUTE] = 'Confirmation Root Page'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id`] = 'Confirmation Root Page'; + +export const CONFIRMATION_V_NEXT_ROUTE = '/confirmation'; +PATH_NAME_MAP[CONFIRMATION_V_NEXT_ROUTE] = 'New Confirmation Page'; +PATH_NAME_MAP[`${CONFIRMATION_V_NEXT_ROUTE}/:id`] = 'New Confirmation Page'; + +export const CONFIRM_SEND_ETHER_PATH = '/send-ether'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_ETHER_PATH}`] = + 'Confirm Send Ether Transaction Page'; + +export const CONFIRM_SEND_TOKEN_PATH = '/send-token'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_TOKEN_PATH}`] = + 'Confirm Send Token Transaction Page'; + +export const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_DEPLOY_CONTRACT_PATH}` +] = 'Confirm Deploy Contract Transaction Page'; + +export const CONFIRM_APPROVE_PATH = '/approve'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_APPROVE_PATH}`] = + 'Confirm Approve Transaction Page'; + +export const CONFIRM_SET_APPROVAL_FOR_ALL_PATH = '/set-approval-for-all'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}` +] = 'Confirm Set Approval For All Transaction Page'; + +export const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TRANSFER_FROM_PATH}`] = + 'Confirm Transfer From Transaction Page'; + +export const CONFIRM_SAFE_TRANSFER_FROM_PATH = '/safe-transfer-from'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SAFE_TRANSFER_FROM_PATH}` +] = 'Confirm Safe Transfer From Transaction Page'; + +export const CONFIRM_TOKEN_METHOD_PATH = '/token-method'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TOKEN_METHOD_PATH}`] = + 'Confirm Token Method Transaction Page'; + +export const CONFIRM_INCREASE_ALLOWANCE_PATH = '/increase-allowance'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_INCREASE_ALLOWANCE_PATH}` +] = 'Confirm Increase Allowance Transaction Page'; + +export const SIGNATURE_REQUEST_PATH = '/signature-request'; +PATH_NAME_MAP[`${CONFIRM_TRANSACTION_ROUTE}/:id${SIGNATURE_REQUEST_PATH}`] = + 'Signature Request Page'; + +export const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${DECRYPT_MESSAGE_REQUEST_PATH}` +] = 'Decrypt Message Request Page'; + +export const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = + '/encryption-public-key-request'; +PATH_NAME_MAP[ + `${CONFIRM_TRANSACTION_ROUTE}/:id${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}` +] = 'Encryption Public Key Request Page'; + +export const CROSS_CHAIN_SWAP_ROUTE = '/cross-chain'; + +export const SWAPS_ROUTE = '/swaps'; + +export const PREPARE_SWAP_ROUTE = '/swaps/prepare-swap-page'; +PATH_NAME_MAP[PREPARE_SWAP_ROUTE] = 'Prepare Swap Page'; + +export const SWAPS_NOTIFICATION_ROUTE = '/swaps/notification-page'; +PATH_NAME_MAP[SWAPS_NOTIFICATION_ROUTE] = 'Swaps Notification Page'; + +export const BUILD_QUOTE_ROUTE = '/swaps/build-quote'; +PATH_NAME_MAP[BUILD_QUOTE_ROUTE] = 'Swaps Build Quote Page'; + +export const VIEW_QUOTE_ROUTE = '/swaps/view-quote'; +PATH_NAME_MAP[VIEW_QUOTE_ROUTE] = 'Swaps View Quotes Page'; + +export const LOADING_QUOTES_ROUTE = '/swaps/loading-quotes'; +PATH_NAME_MAP[LOADING_QUOTES_ROUTE] = 'Swaps Loading Quotes Page'; + +export const AWAITING_SIGNATURES_ROUTE = '/swaps/awaiting-signatures'; + +export const SMART_TRANSACTION_STATUS_ROUTE = '/swaps/smart-transaction-status'; + +export const AWAITING_SWAP_ROUTE = '/swaps/awaiting-swap'; +PATH_NAME_MAP[AWAITING_SWAP_ROUTE] = 'Swaps Awaiting Swaps Page'; + +export const SWAPS_ERROR_ROUTE = '/swaps/swaps-error'; +PATH_NAME_MAP[SWAPS_ERROR_ROUTE] = 'Swaps Error Page'; + +export const SWAPS_MAINTENANCE_ROUTE = '/swaps/maintenance'; + +export const ONBOARDING_ROUTE = '/onboarding'; +export const ONBOARDING_REVIEW_SRP_ROUTE = '/onboarding/review-recovery-phrase'; +export const ONBOARDING_CONFIRM_SRP_ROUTE = + '/onboarding/confirm-recovery-phrase'; +export const ONBOARDING_CREATE_PASSWORD_ROUTE = '/onboarding/create-password'; +export const ONBOARDING_COMPLETION_ROUTE = '/onboarding/completion'; +export const MMI_ONBOARDING_COMPLETION_ROUTE = '/onboarding/account-completion'; +export const ONBOARDING_UNLOCK_ROUTE = '/onboarding/unlock'; +export const ONBOARDING_HELP_US_IMPROVE_ROUTE = '/onboarding/help-us-improve'; +export const ONBOARDING_IMPORT_WITH_SRP_ROUTE = '/onboarding/import-with-recovery-phrase'; -const ONBOARDING_SECURE_YOUR_WALLET_ROUTE = '/onboarding/secure-your-wallet'; -const ONBOARDING_PRIVACY_SETTINGS_ROUTE = '/onboarding/privacy-settings'; -const ONBOARDING_PIN_EXTENSION_ROUTE = '/onboarding/pin-extension'; -const ONBOARDING_WELCOME_ROUTE = '/onboarding/welcome'; -const ONBOARDING_METAMETRICS = '/onboarding/metametrics'; +export const ONBOARDING_SECURE_YOUR_WALLET_ROUTE = + '/onboarding/secure-your-wallet'; +export const ONBOARDING_PRIVACY_SETTINGS_ROUTE = '/onboarding/privacy-settings'; +export const ONBOARDING_PIN_EXTENSION_ROUTE = '/onboarding/pin-extension'; +export const ONBOARDING_WELCOME_ROUTE = '/onboarding/welcome'; +export const ONBOARDING_METAMETRICS = '/onboarding/metametrics'; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) -const INITIALIZE_EXPERIMENTAL_AREA = '/initialize/experimental-area'; -const ONBOARDING_EXPERIMENTAL_AREA = '/onboarding/experimental-area'; +export const INITIALIZE_EXPERIMENTAL_AREA = '/initialize/experimental-area'; +export const ONBOARDING_EXPERIMENTAL_AREA = '/onboarding/experimental-area'; ///: END:ONLY_INCLUDE_IF - -const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'; -const CONFIRM_SEND_ETHER_PATH = '/send-ether'; -const CONFIRM_SEND_TOKEN_PATH = '/send-token'; -const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'; -const CONFIRM_APPROVE_PATH = '/approve'; -const CONFIRM_SET_APPROVAL_FOR_ALL_PATH = '/set-approval-for-all'; -const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'; -const CONFIRM_SAFE_TRANSFER_FROM_PATH = '/safe-transfer-from'; -const CONFIRM_TOKEN_METHOD_PATH = '/token-method'; -const CONFIRM_INCREASE_ALLOWANCE_PATH = '/increase-allowance'; -const SIGNATURE_REQUEST_PATH = '/signature-request'; -const DECRYPT_MESSAGE_REQUEST_PATH = '/decrypt-message-request'; -const ENCRYPTION_PUBLIC_KEY_REQUEST_PATH = '/encryption-public-key-request'; -const CONFIRMATION_V_NEXT_ROUTE = '/confirmation'; - -// Used to pull a convenient name for analytics tracking events. The key must -// be react-router ready path, and can include params such as :id for popup windows -const PATH_NAME_MAP = { - [DEFAULT_ROUTE]: 'Home', - [UNLOCK_ROUTE]: 'Unlock Page', - [LOCK_ROUTE]: 'Lock Page', - [`${ASSET_ROUTE}/:asset/:id`]: `Asset Page`, - [`${ASSET_ROUTE}/image/:asset/:id`]: `Nft Image Page`, - [SETTINGS_ROUTE]: 'Settings Page', - [GENERAL_ROUTE]: 'General Settings Page', - [ADVANCED_ROUTE]: 'Advanced Settings Page', - // DEVELOPER_OPTIONS_ROUTE not included because we're not tracking analytics for this page - // [DEVELOPER_OPTIONS_ROUTE]: 'Experimental Settings Page', - [EXPERIMENTAL_ROUTE]: 'Experimental Settings Page', - [SECURITY_ROUTE]: 'Security Settings Page', - [ABOUT_US_ROUTE]: 'About Us Page', - [ALERTS_ROUTE]: 'Alerts Settings Page', - [NETWORKS_ROUTE]: 'Network Settings Page', - [NETWORKS_FORM_ROUTE]: 'Network Settings Page Form', - [ADD_NETWORK_ROUTE]: 'Add Network From Settings Page Form', - [ADD_POPULAR_CUSTOM_NETWORK]: - 'Add Network From A List Of Popular Custom Networks', - [CONTACT_LIST_ROUTE]: 'Contact List Settings Page', - [`${CONTACT_EDIT_ROUTE}/:address`]: 'Edit Contact Settings Page', - [CONTACT_ADD_ROUTE]: 'Add Contact Settings Page', - [`${CONTACT_VIEW_ROUTE}/:address`]: 'View Contact Settings Page', - [REVEAL_SEED_ROUTE]: 'Reveal Secret Recovery Phrase Page', - [RESTORE_VAULT_ROUTE]: 'Restore Vault Page', - [IMPORT_TOKEN_ROUTE]: 'Import Token Page', - [CONFIRM_IMPORT_TOKEN_ROUTE]: 'Confirm Import Token Page', - [CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE]: 'Confirm Add Suggested Token Page', - [IMPORT_TOKENS_ROUTE]: 'Import Tokens Page', - [NEW_ACCOUNT_ROUTE]: 'New Account Page', - [CONFIRM_ADD_SUGGESTED_NFT_ROUTE]: 'Confirm Add Suggested NFT Page', - [CONNECT_HARDWARE_ROUTE]: 'Connect Hardware Wallet Page', - [NOTIFICATIONS_ROUTE]: 'Notifications Page', - [NOTIFICATIONS_SETTINGS_ROUTE]: 'Notifications Settings Page', - [`${NOTIFICATIONS_ROUTE}/:uuid`]: 'Notification Detail Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAPS_CONNECT_ROUTE}`]: 'Snaps Connect Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAP_INSTALL_ROUTE}`]: 'Snap Install Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAP_UPDATE_ROUTE}`]: 'Snap Update Page', - [`${CONNECT_ROUTE}/:id${CONNECT_SNAP_RESULT_ROUTE}`]: - 'Snap Install Result Page', - [SNAPS_ROUTE]: 'Snaps List Page', - [`${SNAPS_VIEW_ROUTE}/:snapId`]: 'Snap View Page', - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - [INSTITUTIONAL_FEATURES_DONE_ROUTE]: 'Institutional Features Done Page', - [CUSTODY_ACCOUNT_ROUTE]: 'Connect Custody', - [CUSTODY_ACCOUNT_DONE_ROUTE]: 'Connect Custody Account done', - [CONFIRM_ADD_CUSTODIAN_TOKEN]: 'Confirm Add Custodian Token', - [INTERACTIVE_REPLACEMENT_TOKEN_PAGE]: 'Interactive replacement token page', - [SRP_REMINDER]: 'Secret Recovery Phrase Reminder', - ///: END:ONLY_INCLUDE_IF - [SEND_ROUTE]: 'Send Page', - [CONNECTIONS]: 'Connections', - [PERMISSIONS]: 'Permissions', - [`${TOKEN_DETAILS}/:address`]: 'Token Details Page', - [`${CONNECT_ROUTE}/:id`]: 'Connect To Site Confirmation Page', - [`${CONNECT_ROUTE}/:id${CONNECT_CONFIRM_PERMISSIONS_ROUTE}`]: - 'Grant Connected Site Permissions Confirmation Page', - [CONNECTED_ROUTE]: 'Sites Connected To This Account Page', - [CONNECTED_ACCOUNTS_ROUTE]: 'Accounts Connected To This Site Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id`]: 'Confirmation Root Page', - [CONFIRM_TRANSACTION_ROUTE]: 'Confirmation Root Page', - // TODO: rename when this is the only confirmation page - [CONFIRMATION_V_NEXT_ROUTE]: 'New Confirmation Page', - [`${CONFIRMATION_V_NEXT_ROUTE}/:id`]: 'New Confirmation Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TOKEN_METHOD_PATH}`]: - 'Confirm Token Method Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_ETHER_PATH}`]: - 'Confirm Send Ether Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_TOKEN_PATH}`]: - 'Confirm Send Token Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_DEPLOY_CONTRACT_PATH}`]: - 'Confirm Deploy Contract Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_APPROVE_PATH}`]: - 'Confirm Approve Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`]: - 'Confirm Set Approval For All Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_INCREASE_ALLOWANCE_PATH}`]: - 'Confirm Increase Allowance Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TRANSFER_FROM_PATH}`]: - 'Confirm Transfer From Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SAFE_TRANSFER_FROM_PATH}`]: - 'Confirm Safe Transfer From Transaction Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${SIGNATURE_REQUEST_PATH}`]: - 'Signature Request Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${DECRYPT_MESSAGE_REQUEST_PATH}`]: - 'Decrypt Message Request Page', - [`${CONFIRM_TRANSACTION_ROUTE}/:id${ENCRYPTION_PUBLIC_KEY_REQUEST_PATH}`]: - 'Encryption Public Key Request Page', - [BUILD_QUOTE_ROUTE]: 'Swaps Build Quote Page', - [PREPARE_SWAP_ROUTE]: 'Prepare Swap Page', - [SWAPS_NOTIFICATION_ROUTE]: 'Swaps Notification Page', - [VIEW_QUOTE_ROUTE]: 'Swaps View Quotes Page', - [LOADING_QUOTES_ROUTE]: 'Swaps Loading Quotes Page', - [AWAITING_SWAP_ROUTE]: 'Swaps Awaiting Swaps Page', - [SWAPS_ERROR_ROUTE]: 'Swaps Error Page', -}; - -export { - DEFAULT_ROUTE, - ALERTS_ROUTE, - ASSET_ROUTE, - UNLOCK_ROUTE, - LOCK_ROUTE, - SETTINGS_ROUTE, - REVEAL_SEED_ROUTE, - RESTORE_VAULT_ROUTE, - IMPORT_TOKEN_ROUTE, - CONFIRM_IMPORT_TOKEN_ROUTE, - CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, - IMPORT_TOKENS_ROUTE, - NEW_ACCOUNT_ROUTE, - CONFIRM_ADD_SUGGESTED_NFT_ROUTE, - CONNECT_HARDWARE_ROUTE, - SEND_ROUTE, - CONNECTIONS, - PERMISSIONS, - REVIEW_PERMISSIONS, - TOKEN_DETAILS, - CONFIRM_TRANSACTION_ROUTE, - CONFIRM_SEND_ETHER_PATH, - CONFIRM_SEND_TOKEN_PATH, - CONFIRM_DEPLOY_CONTRACT_PATH, - CONFIRM_APPROVE_PATH, - CONFIRM_SET_APPROVAL_FOR_ALL_PATH, - CONFIRM_TRANSFER_FROM_PATH, - CONFIRM_SAFE_TRANSFER_FROM_PATH, - CONFIRM_TOKEN_METHOD_PATH, - CONFIRM_INCREASE_ALLOWANCE_PATH, - SIGNATURE_REQUEST_PATH, - DECRYPT_MESSAGE_REQUEST_PATH, - ENCRYPTION_PUBLIC_KEY_REQUEST_PATH, - CONFIRMATION_V_NEXT_ROUTE, - ADVANCED_ROUTE, - DEVELOPER_OPTIONS_ROUTE, - EXPERIMENTAL_ROUTE, - SECURITY_ROUTE, - GENERAL_ROUTE, - ABOUT_US_ROUTE, - CONTACT_LIST_ROUTE, - CONTACT_EDIT_ROUTE, - CONTACT_ADD_ROUTE, - CONTACT_VIEW_ROUTE, - ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) - CUSTODY_ACCOUNT_DONE_ROUTE, - CUSTODY_ACCOUNT_ROUTE, - INSTITUTIONAL_FEATURES_DONE_ROUTE, - CONFIRM_ADD_CUSTODIAN_TOKEN, - INTERACTIVE_REPLACEMENT_TOKEN_PAGE, - SRP_REMINDER, - ///: END:ONLY_INCLUDE_IF - NETWORKS_ROUTE, - NETWORKS_FORM_ROUTE, - ADD_NETWORK_ROUTE, - ADD_POPULAR_CUSTOM_NETWORK, - CONNECT_ROUTE, - CONNECT_CONFIRM_PERMISSIONS_ROUTE, - CONNECT_SNAPS_CONNECT_ROUTE, - CONNECT_SNAP_INSTALL_ROUTE, - CONNECT_SNAP_UPDATE_ROUTE, - CONNECT_SNAP_RESULT_ROUTE, - NOTIFICATIONS_ROUTE, - NOTIFICATIONS_SETTINGS_ROUTE, - SNAPS_ROUTE, - SNAPS_VIEW_ROUTE, - CROSS_CHAIN_SWAP_ROUTE, - CONNECTED_ROUTE, - CONNECTED_ACCOUNTS_ROUTE, - PATH_NAME_MAP, - SWAPS_ROUTE, - PREPARE_SWAP_ROUTE, - SWAPS_NOTIFICATION_ROUTE, - BUILD_QUOTE_ROUTE, - VIEW_QUOTE_ROUTE, - LOADING_QUOTES_ROUTE, - AWAITING_SWAP_ROUTE, - AWAITING_SIGNATURES_ROUTE, - SWAPS_ERROR_ROUTE, - SWAPS_MAINTENANCE_ROUTE, - SMART_TRANSACTION_STATUS_ROUTE, - ONBOARDING_ROUTE, - ONBOARDING_HELP_US_IMPROVE_ROUTE, - ONBOARDING_CREATE_PASSWORD_ROUTE, - ONBOARDING_IMPORT_WITH_SRP_ROUTE, - ONBOARDING_SECURE_YOUR_WALLET_ROUTE, - ONBOARDING_REVIEW_SRP_ROUTE, - ONBOARDING_CONFIRM_SRP_ROUTE, - ONBOARDING_PRIVACY_SETTINGS_ROUTE, - ONBOARDING_COMPLETION_ROUTE, - MMI_ONBOARDING_COMPLETION_ROUTE, - ONBOARDING_UNLOCK_ROUTE, - ONBOARDING_PIN_EXTENSION_ROUTE, - ONBOARDING_WELCOME_ROUTE, - ONBOARDING_METAMETRICS, - ///: BEGIN:ONLY_INCLUDE_IF(build-flask) - INITIALIZE_EXPERIMENTAL_AREA, - ONBOARDING_EXPERIMENTAL_AREA, - ///: END:ONLY_INCLUDE_IF -}; From 44aa02715fd9ea8665440ac286e7ee3f40880823 Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Tue, 8 Oct 2024 08:19:37 +0100 Subject: [PATCH 12/12] fix: banner alert to render multiple general alerts (#27339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR aims to fix the banner alert to support rendering multiple alerts. Previously we only rendered one alert and if there were more alerts we rendered the banner with a default copy informing the user there are multiple alerts. - Fixed padding on the alerts modal based on [figma](https://www.figma.com/design/gcwF9smHsgvFWQK83lT5UU/Confirmation-redesign-V4?node-id=3355-12480&node-type=frame&t=3Vbe0qFcmcfN5uCG-0) - Fixed bug Contract Interaction and Alerts - 'Cannot read properties of undefined (reading 'key')` <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27339?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2873 https://github.com/MetaMask/metamask-extension/issues/27238 ## **Manual testing steps** 1. Create a transaction with high nonce 2. Go to test dapp 3. Trigger a malicious transaction from PPOM session ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** ![Screenshot from 2024-09-23 13-38-10](https://github.com/user-attachments/assets/f4cbe8ee-7217-4718-998a-2016c9c60b88) ![Screenshot from 2024-09-23 14-09-42](https://github.com/user-attachments/assets/abb8c0c0-8cb8-4230-9469-d0b8b9f2a9a1) ![Screenshot from 2024-09-23 14-21-53](https://github.com/user-attachments/assets/0747e0d0-d50f-4f59-9a9e-0baefb4d9b5e) [bug.webm](https://github.com/user-attachments/assets/eb447959-78f0-4ccc-a554-cca272e59b19) <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Ariella Vu <20778143+digiwand@users.noreply.github.com> --- app/_locales/de/messages.json | 6 -- app/_locales/el/messages.json | 6 -- app/_locales/en/messages.json | 6 -- app/_locales/en_GB/messages.json | 6 -- app/_locales/es/messages.json | 6 -- app/_locales/fr/messages.json | 6 -- app/_locales/hi/messages.json | 6 -- app/_locales/id/messages.json | 6 -- app/_locales/ja/messages.json | 6 -- app/_locales/ko/messages.json | 6 -- app/_locales/pt/messages.json | 6 -- app/_locales/ru/messages.json | 6 -- app/_locales/tl/messages.json | 6 -- app/_locales/tr/messages.json | 6 -- app/_locales/vi/messages.json | 6 -- app/_locales/zh_CN/messages.json | 6 -- .../alert-system/alert-modal/alert-modal.tsx | 9 +- .../app/alert-system/alert-modal/index.scss | 2 - .../confirm-alert-modal.tsx | 9 +- .../general-alert/general-alert.tsx | 2 +- .../multiple-alert-modal.test.tsx | 68 ++++++++++++- .../multiple-alert-modal.tsx | 6 +- ui/hooks/useAlerts.test.ts | 97 +++++++++++++------ ui/hooks/useAlerts.ts | 4 +- .../components/confirm/title/title.test.tsx | 14 ++- .../components/confirm/title/title.tsx | 39 +++----- 26 files changed, 170 insertions(+), 176 deletions(-) diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 92cc0f25f1a7..bda0d4d894e7 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Gas-Optionen aktualisieren" }, - "alertBannerMultipleAlertsDescription": { - "message": "Wenn Sie diese Anfrage genehmigen, könnten Dritte, die für Betrügereien bekannt sind, alle Ihre Assets an sich reißen." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Mehrere Benachrichtigungen!" - }, "alertDisableTooltip": { "message": "Dies kann in „Einstellungen > Benachrichtigungen“ geändert werden." }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index c7f7137665a4..6010f1939602 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Ενημέρωση επιλογών των τελών συναλλαγών" }, - "alertBannerMultipleAlertsDescription": { - "message": "Εάν εγκρίνετε αυτό το αίτημα, ένας τρίτος που είναι γνωστός για απάτες μπορεί να αποκτήσει όλα τα περιουσιακά σας στοιχεία." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Πολλαπλές ειδοποιήσεις!" - }, "alertDisableTooltip": { "message": "Αυτό μπορεί να αλλάξει στις \"Ρυθμίσεις > Ειδοποιήσεις\"" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index de17cf4ea877..30c913d1de74 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -428,12 +428,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Update gas options" }, - "alertBannerMultipleAlertsDescription": { - "message": "If you approve this request, a third party known for scams might take all your assets." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Multiple alerts!" - }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index d02d9b8c1af5..3c8962e7f7c9 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -400,12 +400,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Update gas options" }, - "alertBannerMultipleAlertsDescription": { - "message": "If you approve this request, a third party known for scams might take all your assets." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Multiple alerts!" - }, "alertDisableTooltip": { "message": "This can be changed in \"Settings > Alerts\"" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 3430b44cad96..772471fdfd65 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Actualizar opciones de gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Si aprueba esta solicitud, un tercero conocido por estafas podría quedarse con todos sus activos." - }, - "alertBannerMultipleAlertsTitle": { - "message": "¡Alertas múltiples!" - }, "alertDisableTooltip": { "message": "Esto se puede modificar en \"Configuración > Alertas\"" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index b2429962bad3..4a537a554315 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Mettre à jour les options de gaz" }, - "alertBannerMultipleAlertsDescription": { - "message": "Si vous approuvez cette demande, un tiers connu pour ses activités frauduleuses pourrait s’emparer de tous vos actifs." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Plusieurs alertes !" - }, "alertDisableTooltip": { "message": "Vous pouvez modifier ceci dans « Paramètres > Alertes »" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 91bcfebef973..7fb1a04cb137 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "गैस के विकल्प को अपडेट करें" }, - "alertBannerMultipleAlertsDescription": { - "message": "यदि आप इस रिक्वेस्ट को एप्रूव करते हैं, तो स्कैम के लिए मशहूर कोई थर्ड पार्टी आपके सारे एसेट चुरा सकती है।" - }, - "alertBannerMultipleAlertsTitle": { - "message": "एकाधिक एलर्ट!" - }, "alertDisableTooltip": { "message": "इसे \"सेटिंग > अलर्ट\" में बदला जा सकता है" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 82ab45bdfa99..be3ef95ad448 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Perbarui opsi gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Jika Anda menyetujui permintaan ini, pihak ketiga yang terdeteksi melakukan penipuan dapat mengambil semua aset Anda." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Beberapa peringatan!" - }, "alertDisableTooltip": { "message": "Ini dapat diubah dalam \"Pengaturan > Peringatan\"" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 280889881f57..1ffbc9f1e4eb 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "ガスオプションを更新" }, - "alertBannerMultipleAlertsDescription": { - "message": "このリクエストを承認すると、詐欺が判明しているサードパーティに資産をすべて奪われる可能性があります。" - }, - "alertBannerMultipleAlertsTitle": { - "message": "複数アラート!" - }, "alertDisableTooltip": { "message": "これは「設定」>「アラート」で変更できます" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index c1591b2fc28e..a1c79024f651 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "가스 옵션 업데이트" }, - "alertBannerMultipleAlertsDescription": { - "message": "이 요청을 승인하면 스캠을 목적으로 하는 제3자가 회원님의 자산을 모두 가져갈 수 있습니다." - }, - "alertBannerMultipleAlertsTitle": { - "message": "여러 경고!" - }, "alertDisableTooltip": { "message": "\"설정 > 경고\"에서 변경할 수 있습니다" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 95637cb057f9..52eb392f9d94 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Atualizar opções de gás" }, - "alertBannerMultipleAlertsDescription": { - "message": "Se você aprovar esta solicitação, um terceiro conhecido por aplicar golpes poderá se apropriar de todos os seus ativos." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Vários alertas!" - }, "alertDisableTooltip": { "message": "Isso pode ser alterado em \"Configurações > Alertas\"" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 6ce19f83b4ed..9f4f15461bab 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Обновить параметры газа" }, - "alertBannerMultipleAlertsDescription": { - "message": "Если вы одобрите этот запрос, третья сторона, которая, как известно, совершала мошеннические действия, может похитить все ваши активы." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Множественные оповещения!" - }, "alertDisableTooltip": { "message": "Это можно изменить в разделе «Настройки» > «Оповещения»" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 1246c2a085a1..c2ffc42763d0 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "I-update ang mga opsyon sa gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Kung aaprubahan mo ang kahilingang ito, maaaring kunin ng third party na kilala sa mga panloloko ang lahat asset mo." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Iba't ibang alerto!" - }, "alertDisableTooltip": { "message": "Puwede itong baguhin sa \"Mga Setting > Mga Alerto\"" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index eedc60659269..676896deaaae 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Gaz seçeneklerini güncelle" }, - "alertBannerMultipleAlertsDescription": { - "message": "Bu talebi onaylarsanız dolandırıcılıkla bilinen üçüncü bir taraf tüm varlıklarınızı çalabilir." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Çoklu uyarı!" - }, "alertDisableTooltip": { "message": "\"Ayarlar > Uyarılar\" kısmında değiştirilebilir" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 955c302f19a8..442478665c00 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "Cập nhật tùy chọn phí gas" }, - "alertBannerMultipleAlertsDescription": { - "message": "Nếu bạn chấp thuận yêu cầu này, một bên thứ ba nổi tiếng là lừa đảo có thể lấy hết tài sản của bạn." - }, - "alertBannerMultipleAlertsTitle": { - "message": "Có nhiều cảnh báo!" - }, "alertDisableTooltip": { "message": "Bạn có thể thay đổi trong phần \"Cài đặt > Cảnh báo\"" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 14395afca8b8..9f33ef4a6b35 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -381,12 +381,6 @@ "alertActionUpdateGasFeeLevel": { "message": "更新燃料选项" }, - "alertBannerMultipleAlertsDescription": { - "message": "如果您批准此请求,以欺诈闻名的第三方可能会拿走您的所有资产。" - }, - "alertBannerMultipleAlertsTitle": { - "message": "多个提醒!" - }, "alertDisableTooltip": { "message": "这可以在“设置 > 提醒”中进行更改" }, diff --git a/ui/components/app/alert-system/alert-modal/alert-modal.tsx b/ui/components/app/alert-system/alert-modal/alert-modal.tsx index 46fbe1f8b8e3..10f5d90c3e77 100644 --- a/ui/components/app/alert-system/alert-modal/alert-modal.tsx +++ b/ui/components/app/alert-system/alert-modal/alert-modal.tsx @@ -157,10 +157,9 @@ function AlertDetails({ <Box key={selectedAlert.key} display={Display.InlineBlock} - padding={2} + padding={customDetails ? 0 : 2} width={BlockSize.Full} backgroundColor={customDetails ? undefined : severityStyle.background} - gap={2} borderRadius={BorderRadius.SM} > {customDetails ?? ( @@ -209,12 +208,11 @@ export function AcknowledgeCheckboxBase({ return ( <Box display={Display.Flex} - padding={3} + padding={4} width={BlockSize.Full} - gap={3} backgroundColor={severityStyle.background} - marginTop={4} borderRadius={BorderRadius.LG} + marginTop={4} > <Checkbox label={label ?? t('alertModalAcknowledge')} @@ -375,6 +373,7 @@ export function AlertModal({ display={Display.Flex} flexDirection={FlexDirection.Column} gap={4} + paddingTop={2} width={BlockSize.Full} > {customAcknowledgeButton ?? ( diff --git a/ui/components/app/alert-system/alert-modal/index.scss b/ui/components/app/alert-system/alert-modal/index.scss index 722dbf763446..c9100ae95345 100644 --- a/ui/components/app/alert-system/alert-modal/index.scss +++ b/ui/components/app/alert-system/alert-modal/index.scss @@ -8,7 +8,5 @@ &__acknowledge-checkbox { @include design-system.H6; - - padding-top: 2px; } } diff --git a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx index 96bcebab9953..f84c8113ae1e 100644 --- a/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx +++ b/ui/components/app/alert-system/confirm-alert-modal/confirm-alert-modal.tsx @@ -90,8 +90,7 @@ function ConfirmDetails({ {t('confirmationAlertModalDetails')} </Text> <ButtonLink - paddingTop={5} - paddingBottom={5} + marginTop={4} size={ButtonLinkSize.Inherit} textProps={{ variant: TextVariant.bodyMd, @@ -103,11 +102,7 @@ function ConfirmDetails({ rel="noopener noreferrer" data-testid="confirm-alert-modal-review-all-alerts" > - <Icon - name={IconName.SecuritySearch} - size={IconSize.Inherit} - marginLeft={1} - /> + <Icon name={IconName.SecuritySearch} size={IconSize.Inherit} /> {t('alertModalReviewAllAlerts')} </ButtonLink> </Box> diff --git a/ui/components/app/alert-system/general-alert/general-alert.tsx b/ui/components/app/alert-system/general-alert/general-alert.tsx index 3ba74445acef..5ac2b2a335fb 100644 --- a/ui/components/app/alert-system/general-alert/general-alert.tsx +++ b/ui/components/app/alert-system/general-alert/general-alert.tsx @@ -27,7 +27,7 @@ export type GeneralAlertProps = { provider?: SecurityProvider; reportUrl?: string; severity: AlertSeverity; - title: string; + title?: string; }; function ReportLink({ diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx index c4b79fb28b7c..3d176e57ccd0 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx @@ -4,6 +4,7 @@ import { fireEvent } from '@testing-library/react'; import { Severity } from '../../../../helpers/constants/design-system'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import mockState from '../../../../../test/data/mock-state.json'; +import * as useAlertsModule from '../../../../hooks/useAlerts'; import { MultipleAlertModal, MultipleAlertModalProps, @@ -84,6 +85,56 @@ describe('MultipleAlertModal', () => { }, }); + it('defaults to the first alert if the selected alert is not found', async () => { + const setAlertConfirmedMock = jest.fn(); + const useAlertsSpy = jest.spyOn(useAlertsModule, 'default'); + const dangerAlertMock = alertsMock.find( + (alert) => alert.key === DATA_ALERT_KEY_MOCK, + ); + (useAlertsSpy as jest.Mock).mockReturnValue({ + setAlertConfirmed: setAlertConfirmedMock, + alerts: alertsMock, + generalAlerts: [], + fieldAlerts: alertsMock, + getFieldAlerts: () => alertsMock, + isAlertConfirmed: () => false, + }); + + const { getByText, queryByText, rerender } = renderWithProvider( + <MultipleAlertModal + {...defaultProps} + alertKey={CONTRACT_ALERT_KEY_MOCK} + />, + mockStore, + ); + + // shows the contract alert + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + + // Update the mock to return only the data alert + (useAlertsSpy as jest.Mock).mockReturnValue({ + setAlertConfirmed: setAlertConfirmedMock, + alerts: [dangerAlertMock], + generalAlerts: [], + fieldAlerts: [dangerAlertMock], + getFieldAlerts: () => [dangerAlertMock], + isAlertConfirmed: () => false, + }); + + // Rerender the component to apply the updated mock + rerender( + <MultipleAlertModal + {...defaultProps} + alertKey={CONTRACT_ALERT_KEY_MOCK} + />, + ); + + // verifies the data alert is shown + expect(queryByText(alertsMock[0].message)).not.toBeInTheDocument(); + expect(getByText(alertsMock[1].message)).toBeInTheDocument(); + useAlertsSpy.mockRestore(); + }); + it('renders the multiple alert modal', () => { const { getByTestId } = renderWithProvider( <MultipleAlertModal {...defaultProps} />, @@ -107,7 +158,7 @@ describe('MultipleAlertModal', () => { expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1); }); - it('render the next alert when the "Got it" button is clicked', () => { + it('renders the next alert when the "Got it" button is clicked', () => { const { getByTestId, getByText } = renderWithProvider( <MultipleAlertModal {...defaultProps} alertKey={DATA_ALERT_KEY_MOCK} />, mockStoreAcknowledgeAlerts, @@ -134,6 +185,20 @@ describe('MultipleAlertModal', () => { expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1); }); + it('resets to the first alert if there are unconfirmed alerts and the final alert is acknowledged', () => { + const { getByTestId, getByText } = renderWithProvider( + <MultipleAlertModal + {...defaultProps} + alertKey={CONTRACT_ALERT_KEY_MOCK} + />, + mockStore, + ); + + fireEvent.click(getByTestId('alert-modal-button')); + + expect(getByText(alertsMock[0].message)).toBeInTheDocument(); + }); + describe('Navigation', () => { it('calls next alert when the next button is clicked', () => { const { getByTestId, getByText } = renderWithProvider( @@ -144,6 +209,7 @@ describe('MultipleAlertModal', () => { fireEvent.click(getByTestId('alert-modal-next-button')); expect(getByText(alertsMock[2].message)).toBeInTheDocument(); + expect(getByText(alertsMock[2].message)).toBeInTheDocument(); }); it('calls previous alert when the previous button is clicked', () => { diff --git a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx index d3b289343d00..62875bffcfe0 100644 --- a/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx +++ b/ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx @@ -162,7 +162,9 @@ export function MultipleAlertModal({ initialAlertIndex === -1 ? 0 : initialAlertIndex, ); - const selectedAlert = alerts[selectedIndex]; + // If the selected alert is not found, default to the first alert + const selectedAlert = alerts[selectedIndex] ?? alerts[0]; + const hasUnconfirmedAlerts = alerts.some( (alert: Alert) => !isAlertConfirmed(alert.key) && alert.severity === Severity.Danger, @@ -207,7 +209,7 @@ export function MultipleAlertModal({ <AlertModal ownerId={ownerId} onAcknowledgeClick={handleAcknowledgeClick} - alertKey={selectedAlert.key} + alertKey={selectedAlert?.key} onClose={onClose} headerStartAccessory={ <PageNavigation diff --git a/ui/hooks/useAlerts.test.ts b/ui/hooks/useAlerts.test.ts index 0e9687a6d874..94f1bb247541 100644 --- a/ui/hooks/useAlerts.test.ts +++ b/ui/hooks/useAlerts.test.ts @@ -56,10 +56,16 @@ describe('useAlerts', () => { ); }; - const { result } = renderHookUseAlert(); + const renderAndReturnResult = ( + ownerId?: string, + state?: { confirmAlerts: ConfirmAlertsState }, + ) => { + return renderHookUseAlert(ownerId, state).result; + }; describe('alerts', () => { it('returns all alerts', () => { + const result = renderAndReturnResult(); expect(result.current.alerts).toEqual(alertsMock); expect(result.current.hasAlerts).toEqual(true); expect(result.current.hasDangerAlerts).toEqual(true); @@ -67,6 +73,7 @@ describe('useAlerts', () => { }); it('returns alerts ordered by severity', () => { + const result = renderAndReturnResult(); const orderedAlerts = result.current.alerts; expect(orderedAlerts[0].severity).toEqual(Severity.Danger); }); @@ -74,7 +81,7 @@ describe('useAlerts', () => { describe('unconfirmedDangerAlerts', () => { it('returns all unconfirmed danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -83,15 +90,15 @@ describe('useAlerts', () => { confirmed: {}, }, }); - expect(result1.current.hasAlerts).toEqual(true); - expect(result1.current.hasUnconfirmedDangerAlerts).toEqual(true); - expect(result1.current.unconfirmedDangerAlerts).toHaveLength(1); + expect(result.current.hasAlerts).toEqual(true); + expect(result.current.hasUnconfirmedDangerAlerts).toEqual(true); + expect(result.current.unconfirmedDangerAlerts).toHaveLength(1); }); }); describe('unconfirmedFieldDangerAlerts', () => { it('returns all unconfirmed field danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -112,7 +119,7 @@ describe('useAlerts', () => { alert.field === fromAlertKeyMock && alert.severity === Severity.Danger, ); - expect(result1.current.unconfirmedFieldDangerAlerts).toEqual([ + expect(result.current.unconfirmedFieldDangerAlerts).toEqual([ expectedFieldDangerAlert, ]); }); @@ -120,7 +127,7 @@ describe('useAlerts', () => { describe('hasUnconfirmedFieldDangerAlerts', () => { it('returns true if there are unconfirmed field danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -136,11 +143,11 @@ describe('useAlerts', () => { }, }, }); - expect(result1.current.hasUnconfirmedFieldDangerAlerts).toEqual(true); + expect(result.current.hasUnconfirmedFieldDangerAlerts).toEqual(true); }); it('returns false if there are no unconfirmed field danger alerts', () => { - const { result: result1 } = renderHookUseAlert(undefined, { + const result = renderAndReturnResult(undefined, { confirmAlerts: { alerts: { [ownerIdMock]: alertsMock, @@ -156,16 +163,43 @@ describe('useAlerts', () => { }, }, }); - expect(result1.current.hasUnconfirmedFieldDangerAlerts).toEqual(false); + expect(result.current.hasUnconfirmedFieldDangerAlerts).toEqual(false); }); }); describe('generalAlerts', () => { - it('returns general alerts', () => { - const expectedGeneralAlerts = alertsMock.find( - (alert) => alert.key === dataAlertKeyMock, - ); - expect(result.current.generalAlerts).toEqual([expectedGeneralAlerts]); + it('returns general alerts sorted by severity', () => { + const warningGeneralAlert = { + key: dataAlertKeyMock, + severity: Severity.Warning as AlertSeverity, + message: 'Alert 2', + }; + const expectedGeneralAlerts = [ + { + ...warningGeneralAlert, + severity: Severity.Info as AlertSeverity, + message: 'Alert 3', + key: fromAlertKeyMock, + }, + { + ...warningGeneralAlert, + severity: Severity.Danger as AlertSeverity, + message: 'Alert 1', + key: toAlertKeyMock, + }, + warningGeneralAlert, + ]; + + const result = renderAndReturnResult(undefined, { + confirmAlerts: { + alerts: { + [ownerIdMock]: expectedGeneralAlerts, + }, + confirmed: {}, + }, + }); + + expect(result.current.generalAlerts).toEqual(expectedGeneralAlerts); }); }); @@ -174,22 +208,26 @@ describe('useAlerts', () => { (alert) => alert.field === fromAlertKeyMock, ); it('returns all alert filtered by field property', () => { + const result = renderAndReturnResult(); expect(result.current.getFieldAlerts(fromAlertKeyMock)).toEqual([ expectedFieldAlerts, ]); }); it('returns empty array if field is not provided', () => { + const result = renderAndReturnResult(); expect(result.current.getFieldAlerts()).toEqual([]); }); it('returns empty array, when no alert for specified field', () => { + const result = renderAndReturnResult(); expect(result.current.getFieldAlerts('mockedField')).toEqual([]); }); }); describe('fieldAlerts', () => { it('returns all alerts with field property', () => { + const result = renderAndReturnResult(); expect(result.current.fieldAlerts).toEqual([ alertsMock[0], alertsMock[2], @@ -197,38 +235,33 @@ describe('useAlerts', () => { }); it('returns empty array if no alerts with field property', () => { - const { result: resultAlerts } = renderHookUseAlert('mockedOwnerId'); - expect(resultAlerts.current.fieldAlerts).toEqual([]); + const result = renderAndReturnResult('mockedOwnerId'); + expect(result.current.fieldAlerts).toEqual([]); }); }); describe('isAlertConfirmed', () => { it('returns an if an alert is confirmed', () => { + const result = renderAndReturnResult(); expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(true); }); it('returns an if an alert is not confirmed', () => { - const { result: resultAlerts } = renderHookUseAlert(ownerId2Mock); - expect(resultAlerts.current.isAlertConfirmed(fromAlertKeyMock)).toBe( - false, - ); + const result = renderAndReturnResult(ownerId2Mock); + expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(false); }); }); describe('setAlertConfirmed', () => { it('dismisses alert confirmation', () => { - const { result: resultAlerts } = renderHookUseAlert(); - resultAlerts.current.setAlertConfirmed(fromAlertKeyMock, false); - expect(resultAlerts.current.isAlertConfirmed(fromAlertKeyMock)).toBe( - false, - ); + const result = renderAndReturnResult(); + result.current.setAlertConfirmed(fromAlertKeyMock, false); + expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(false); }); it('confirms an alert', () => { - const { result: resultAlerts } = renderHookUseAlert(ownerId2Mock); - resultAlerts.current.setAlertConfirmed(fromAlertKeyMock, true); - expect(resultAlerts.current.isAlertConfirmed(fromAlertKeyMock)).toBe( - true, - ); + const result = renderAndReturnResult(ownerId2Mock); + result.current.setAlertConfirmed(fromAlertKeyMock, true); + expect(result.current.isAlertConfirmed(fromAlertKeyMock)).toBe(true); }); }); }); diff --git a/ui/hooks/useAlerts.ts b/ui/hooks/useAlerts.ts index 06d79800f634..4b8e74a46d42 100644 --- a/ui/hooks/useAlerts.ts +++ b/ui/hooks/useAlerts.ts @@ -24,8 +24,8 @@ const useAlerts = (ownerId: string) => { selectConfirmedAlertKeys(state as AlertsState, ownerId), ); - const generalAlerts = useSelector((state) => - selectGeneralAlerts(state as AlertsState, ownerId), + const generalAlerts = sortAlertsBySeverity( + useSelector((state) => selectGeneralAlerts(state as AlertsState, ownerId)), ); const fieldAlerts = sortAlertsBySeverity( diff --git a/ui/pages/confirmations/components/confirm/title/title.test.tsx b/ui/pages/confirmations/components/confirm/title/title.test.tsx index eeaab80fd46b..3c03343c2afb 100644 --- a/ui/pages/confirmations/components/confirm/title/title.test.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.test.tsx @@ -162,16 +162,23 @@ describe('ConfirmTitle', () => { reason: 'mock reason', key: 'mock key', }; + + const alertMock2 = { + ...alertMock, + key: 'mock key 2', + reason: 'mock reason 2', + }; const mockAlertState = (state: Partial<ConfirmAlertsState> = {}) => getMockPersonalSignConfirmStateForRequest(unapprovedPersonalSignMsg, { metamask: {}, confirmAlerts: { alerts: { - [unapprovedPersonalSignMsg.id]: [alertMock, alertMock, alertMock], + [unapprovedPersonalSignMsg.id]: [alertMock, alertMock2], }, confirmed: { [unapprovedPersonalSignMsg.id]: { [alertMock.key]: false, + [alertMock2.key]: false, }, }, ...state, @@ -194,7 +201,7 @@ describe('ConfirmTitle', () => { expect(queryByText(alertMock.message)).toBeInTheDocument(); }); - it('renders alert banner when there are multiple alerts', () => { + it('renders multiple alert banner when there are multiple alerts', () => { const mockStore = configureMockStore([])(mockAlertState()); const { getByText } = renderWithConfirmContextProvider( @@ -202,7 +209,8 @@ describe('ConfirmTitle', () => { mockStore, ); - expect(getByText('Multiple alerts!')).toBeInTheDocument(); + expect(getByText(alertMock.reason)).toBeInTheDocument(); + expect(getByText(alertMock2.reason)).toBeInTheDocument(); }); }); }); diff --git a/ui/pages/confirmations/components/confirm/title/title.tsx b/ui/pages/confirmations/components/confirm/title/title.tsx index 702c496b4e25..2645feed8a41 100644 --- a/ui/pages/confirmations/components/confirm/title/title.tsx +++ b/ui/pages/confirmations/components/confirm/title/title.tsx @@ -4,7 +4,6 @@ import { } from '@metamask/transaction-controller'; import React, { memo, useMemo } from 'react'; import GeneralAlert from '../../../../../components/app/alert-system/general-alert/general-alert'; -import { getHighestSeverity } from '../../../../../components/app/alert-system/utils'; import { Box, Text } from '../../../../../components/component-library'; import { TextAlign, @@ -25,37 +24,27 @@ import { getIsRevokeSetApprovalForAll } from '../info/utils'; import { useCurrentSpendingCap } from './hooks/useCurrentSpendingCap'; function ConfirmBannerAlert({ ownerId }: { ownerId: string }) { - const t = useI18nContext(); const { generalAlerts } = useAlerts(ownerId); if (generalAlerts.length === 0) { return null; } - const hasMultipleAlerts = generalAlerts.length > 1; - const singleAlert = generalAlerts[0]; - const highestSeverity = hasMultipleAlerts - ? getHighestSeverity(generalAlerts) - : singleAlert.severity; return ( - <Box marginTop={4}> - <GeneralAlert - data-testid="confirm-banner-alert" - title={ - hasMultipleAlerts - ? t('alertBannerMultipleAlertsTitle') - : singleAlert.reason - } - description={ - hasMultipleAlerts - ? t('alertBannerMultipleAlertsDescription') - : singleAlert.message - } - severity={highestSeverity} - provider={hasMultipleAlerts ? undefined : singleAlert.provider} - details={hasMultipleAlerts ? undefined : singleAlert.alertDetails} - reportUrl={singleAlert.reportUrl} - /> + <Box marginTop={3}> + {generalAlerts.map((alert) => ( + <Box marginTop={1} key={alert.key}> + <GeneralAlert + data-testid="confirm-banner-alert" + title={alert.reason} + description={alert.message} + severity={alert.severity} + provider={alert.provider} + details={alert.alertDetails} + reportUrl={alert.reportUrl} + /> + </Box> + ))} </Box> ); }