From 23fe8751e120a9a36ea427d29860f00067865cab Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Thu, 2 Nov 2023 16:29:35 +0100 Subject: [PATCH 01/18] Tests: Make tests independent (#2736) --- cypress/e2e/pages/address_book.page.js | 7 +++++++ cypress/e2e/pages/batches.pages.js | 2 +- cypress/e2e/pages/load_safe.pages.js | 4 ++++ cypress/e2e/safe-apps/info_modal.cy.js | 2 +- cypress/e2e/safe-apps/preview_drawer.cy.js | 2 +- cypress/e2e/safe-apps/tx_modal.cy.js | 4 +--- cypress/e2e/smoke/address_book.cy.js | 15 +++++++++------ cypress/e2e/smoke/balances.cy.js | 3 ++- cypress/e2e/smoke/load_safe.cy.js | 20 +++++++++++++++----- cypress/e2e/smoke/tx_history.cy.js | 4 +--- 10 files changed, 42 insertions(+), 21 deletions(-) diff --git a/cypress/e2e/pages/address_book.page.js b/cypress/e2e/pages/address_book.page.js index 29ea8e69de..2906d58290 100644 --- a/cypress/e2e/pages/address_book.page.js +++ b/cypress/e2e/pages/address_book.page.js @@ -67,6 +67,13 @@ export function verifyNewEntryAdded(name, address) { cy.contains(address).should('exist') } +export function addEntry(name, address) { + typeInName(name) + typeInAddress(address) + clickOnSaveEntryBtn() + verifyNewEntryAdded(name, address) +} + export function clickOnEditEntryBtn() { cy.get(editEntryBtn).click({ force: true }) } diff --git a/cypress/e2e/pages/batches.pages.js b/cypress/e2e/pages/batches.pages.js index d1c5628475..85e98e6350 100644 --- a/cypress/e2e/pages/batches.pages.js +++ b/cypress/e2e/pages/batches.pages.js @@ -89,7 +89,7 @@ export function verifyAmountTransactionsInBatch(count) { } export function clickOnConfirmBatchBtn() { - cy.contains(confirmBatchBtn).click() + cy.get('button').contains(confirmBatchBtn).should('be.visible').should('be.enabled').click() } export function verifyBatchTransactionsCount(count) { diff --git a/cypress/e2e/pages/load_safe.pages.js b/cypress/e2e/pages/load_safe.pages.js index 9ad46717d5..bc0fa83daa 100644 --- a/cypress/e2e/pages/load_safe.pages.js +++ b/cypress/e2e/pages/load_safe.pages.js @@ -39,6 +39,10 @@ export function selectPolygon() { cy.contains('span', constants.networks.polygon) } +export function inputNameAndAddress(name, address) { + inputName(name) + inputAddress(address) +} export function inputName(name) { cy.get(nameInput).type(name).should('have.value', name) } diff --git a/cypress/e2e/safe-apps/info_modal.cy.js b/cypress/e2e/safe-apps/info_modal.cy.js index 63e45f1b0e..650fe1b041 100644 --- a/cypress/e2e/safe-apps/info_modal.cy.js +++ b/cypress/e2e/safe-apps/info_modal.cy.js @@ -3,7 +3,7 @@ import * as main from '../pages/main.page' import * as safeapps from '../pages/safeapps.pages' describe('Safe Apps info modal tests', () => { - before(() => { + beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.SEPOLIA_TEST_SAFE_5 + constants.appsUrl, { failOnStatusCode: false }) main.acceptCookies() diff --git a/cypress/e2e/safe-apps/preview_drawer.cy.js b/cypress/e2e/safe-apps/preview_drawer.cy.js index bae5b9ca94..985a1bcad6 100644 --- a/cypress/e2e/safe-apps/preview_drawer.cy.js +++ b/cypress/e2e/safe-apps/preview_drawer.cy.js @@ -3,7 +3,7 @@ import * as main from '../pages/main.page' import * as safeapps from '../pages/safeapps.pages' describe('Safe Apps info modal tests', () => { - before(() => { + beforeEach(() => { cy.clearLocalStorage() cy.visit(`/${constants.SEPOLIA_TEST_SAFE_5}/apps`, { failOnStatusCode: false }) main.acceptCookies() diff --git a/cypress/e2e/safe-apps/tx_modal.cy.js b/cypress/e2e/safe-apps/tx_modal.cy.js index 44941ef2bd..b2961038a1 100644 --- a/cypress/e2e/safe-apps/tx_modal.cy.js +++ b/cypress/e2e/safe-apps/tx_modal.cy.js @@ -6,10 +6,8 @@ const testAppName = 'Cypress Test App' const testAppDescr = 'Cypress Test App Description' describe('Transaction modal tests', () => { - before(() => { - cy.clearLocalStorage() - }) beforeEach(() => { + cy.clearLocalStorage() cy.fixture('safe-app').then((html) => { cy.intercept('GET', `${constants.testAppUrl}/*`, html) cy.intercept('GET', `*/manifest.json`, { diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index 21481837e4..3a5af18932 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -9,21 +9,20 @@ const NAME = 'Owner1' const EDITED_NAME = 'Edited Owner1' describe('Address book tests', () => { - before(() => { + beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) - main.acceptCookies() + main.acceptCookies(1) }) it('Verify entry can be added [C56061]', () => { addressBook.clickOnCreateEntryBtn() - addressBook.typeInName(NAME) - addressBook.typeInAddress(constants.RECIPIENT_ADDRESS) - addressBook.clickOnSaveEntryBtn() - addressBook.verifyNewEntryAdded(NAME, constants.RECIPIENT_ADDRESS) + addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) }) it('Verify entered entry in Name input can be saved [C56063]', () => { + addressBook.clickOnCreateEntryBtn() + addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) addressBook.clickOnEditEntryBtn() addressBook.typeInNameInput(EDITED_NAME) addressBook.clickOnSaveButton() @@ -31,6 +30,8 @@ describe('Address book tests', () => { }) it('Verify entry can be deleted [C56062]', () => { + addressBook.clickOnCreateEntryBtn() + addressBook.addEntry(NAME, constants.RECIPIENT_ADDRESS) // Click the delete button in the first entry addressBook.clickDeleteEntryButton() addressBook.clickDeleteEntryModalDeleteButton() @@ -63,6 +64,8 @@ describe('Address book tests', () => { }) it('Verify the address book file can be downloaded [C56065]', () => { + addressBook.clickOnImportFileBtn() + addressBook.importFile() // Download the export file const date = format(new Date(), 'yyyy-MM-dd', { timeZone: 'UTC' }) const fileName = `safe-address-book-${date}.csv` //name that is given to the file automatically diff --git a/cypress/e2e/smoke/balances.cy.js b/cypress/e2e/smoke/balances.cy.js index 47142aa9f5..df8e1c3681 100644 --- a/cypress/e2e/smoke/balances.cy.js +++ b/cypress/e2e/smoke/balances.cy.js @@ -11,7 +11,7 @@ describe('Balance tests', () => { // Fiat balance regex const fiatRegex = balances.fiatRegex - before(() => { + beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) main.acceptCookies(2) @@ -114,6 +114,7 @@ describe('Balance tests', () => { }) it('Verify a token can be unhidden [C56081]', () => { + balances.hideAsset(balances.currencyDaiCap) balances.openHideTokenMenu() balances.clickOnTokenCheckbox(balances.currencyDaiCap) balances.saveHiddenTokenSelection() diff --git a/cypress/e2e/smoke/load_safe.cy.js b/cypress/e2e/smoke/load_safe.cy.js index 0ff6a0c0c8..8aaa66bede 100644 --- a/cypress/e2e/smoke/load_safe.cy.js +++ b/cypress/e2e/smoke/load_safe.cy.js @@ -19,7 +19,7 @@ const OWNER_ENS_DEFAULT_NAME = 'test20.eth' const OWNER_ADDRESS = constants.EOA describe('Load Safe tests', () => { - before(() => { + beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.loadNewSafeSepoliaUrl) main.acceptCookies() @@ -39,10 +39,8 @@ describe('Load Safe tests', () => { cy.get('input[name="address"]').parent().prev('label').as('addressLabel') createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) - - safe.inputName(testSafeName) safe.verifyIncorrectAddressErrorMessage() - safe.inputAddress(constants.SEPOLIA_TEST_SAFE_1) + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) // Type an invalid address // cy.get('input[name="address"]').clear().type(EOA_ADDRESS) @@ -65,17 +63,29 @@ describe('Load Safe tests', () => { }) it('Verify custom name in the first owner an be set [C56120]', () => { + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + safe.clickOnNextBtn() createwallet.typeOwnerName(testOwnerName, 0) safe.clickOnNextBtn() }) it('Verify Safe and owner names are displayed in the Review step [C56121]', () => { + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_1) + safe.clickOnNextBtn() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() safe.verifyDataInReviewSection(testSafeName, testOwnerName) safe.clickOnAddBtn() }) it('Verify the custom Safe name is successfully loaded [C56122]', () => { - main.verifyHomeSafeUrl(constants.SEPOLIA_TEST_SAFE_1) + safe.inputNameAndAddress(testSafeName, constants.SEPOLIA_TEST_SAFE_2) + safe.clickOnNextBtn() + createwallet.typeOwnerName(testOwnerName, 0) + safe.clickOnNextBtn() + safe.verifyDataInReviewSection(testSafeName, testOwnerName) + safe.clickOnAddBtn() + main.verifyHomeSafeUrl(constants.SEPOLIA_TEST_SAFE_2) safe.veriySidebarSafeNameIsVisible(testSafeName) safe.verifyOwnerNamePresentInSettings(testOwnerName) }) diff --git a/cypress/e2e/smoke/tx_history.cy.js b/cypress/e2e/smoke/tx_history.cy.js index cbc49612ed..42fdbeeb44 100644 --- a/cypress/e2e/smoke/tx_history.cy.js +++ b/cypress/e2e/smoke/tx_history.cy.js @@ -20,11 +20,10 @@ describe('Transaction history tests', () => { main.acceptCookies(1) }) - it('Verify October 9th transactions are displayed [C56128]', () => { + it('Verify October 29th transactions are displayed [C56128]', () => { const DATE = 'Oct 29, 2023' const NEXT_DATE_LABEL = 'Oct 20, 2023' const amount = '0.00001 ETH' - const time = '10:39 PM' const success = 'Success' createTx.verifyDateExists(DATE) @@ -42,7 +41,6 @@ describe('Transaction history tests', () => { // Info createTx.verifyImageAltTxt(1, constants.tokenAbbreviation.sep) createTx.verifyTransactionStrExists(amount) - createTx.verifyTransactionStrExists(time) createTx.verifyTransactionStrExists(success) }) }) From cb5da935b272ee428521f159a17a7ded1303441f Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:56:17 +0100 Subject: [PATCH 02/18] Tests: Update workflow to enable tests run in parallel (#2739) * Update workflow to enable tests run in parallel * Update workflow of Safe Apps to run in parallel --- .github/workflows/e2e.yml | 9 ++++++++- .github/workflows/safe-apps-e2e.yml | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6af7316dbc..fc2c709f25 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -10,7 +10,11 @@ concurrency: jobs: e2e: runs-on: ubuntu-20.04 - name: Smoke E2E tests + name: Cypress Smoke tests + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3] steps: - uses: actions/checkout@v3 @@ -30,9 +34,12 @@ jobs: - uses: cypress-io/github-action@v4 with: + parallel: true spec: cypress/e2e/smoke/*.cy.js browser: chrome record: true config: baseUrl=http://localhost:8080 + group: 'Smoke tests' env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/safe-apps-e2e.yml b/.github/workflows/safe-apps-e2e.yml index 6067184867..7053c80d4d 100644 --- a/.github/workflows/safe-apps-e2e.yml +++ b/.github/workflows/safe-apps-e2e.yml @@ -11,7 +11,11 @@ concurrency: jobs: e2e: runs-on: ubuntu-latest - name: Safe Apps E2E tests + name: Cypress Safe Apps tests + strategy: + fail-fast: false + matrix: + containers: [1, 2, 3] steps: - uses: actions/checkout@v3 @@ -31,10 +35,13 @@ jobs: - uses: cypress-io/github-action@v4 with: + parallel: true spec: cypress/e2e/safe-apps/*.cy.js browser: chrome record: true config: baseUrl=http://localhost:8080 + group: 'Safe Apps tests' env: CYPRESS_PROJECT_ID: okn21k CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_SAFE_APPS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2fce8de48cd825650f45362b041d44d6ab1ecb99 Mon Sep 17 00:00:00 2001 From: Michael <30682308+mike10ca@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:16:01 +0100 Subject: [PATCH 03/18] Tests: Add safe apps tests (#2751) * Add safe apps. Fix balance pagination * Update test definitions * Update tooltip expecation in Assets tests --- cypress.config.js | 2 +- cypress/e2e/pages/safeapps.pages.js | 65 +++++ .../e2e/safe-apps/drain_account.spec.cy.js | 93 +++++++ cypress/e2e/safe-apps/tx-builder.spec.cy.js | 252 ++++++++++++++++++ cypress/e2e/smoke/assets.cy.js | 2 +- cypress/e2e/smoke/balances_pagination.cy.js | 6 +- cypress/fixtures/balances.json | 96 +++++++ cypress/fixtures/test-empty-batch.json | 1 + cypress/fixtures/test-invalid-batch.json | 3 + cypress/fixtures/test-mainnet-batch.json | 37 +++ cypress/fixtures/test-modified-batch.json | 37 +++ cypress/fixtures/test-working-batch.json | 37 +++ cypress/support/commands.js | 154 +++++++++++ cypress/support/constants.js | 9 + 14 files changed, 788 insertions(+), 6 deletions(-) create mode 100644 cypress/e2e/safe-apps/drain_account.spec.cy.js create mode 100644 cypress/e2e/safe-apps/tx-builder.spec.cy.js create mode 100644 cypress/fixtures/balances.json create mode 100644 cypress/fixtures/test-empty-batch.json create mode 100644 cypress/fixtures/test-invalid-batch.json create mode 100644 cypress/fixtures/test-mainnet-batch.json create mode 100644 cypress/fixtures/test-modified-batch.json create mode 100644 cypress/fixtures/test-working-batch.json diff --git a/cypress.config.js b/cypress.config.js index 753ec36daa..ff3bf4baf8 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -5,7 +5,7 @@ export default defineConfig({ trashAssetsBeforeRuns: true, retries: { - runMode: 3, + runMode: 1, openMode: 0, }, diff --git a/cypress/e2e/pages/safeapps.pages.js b/cypress/e2e/pages/safeapps.pages.js index 1385761e8b..fb7401609b 100644 --- a/cypress/e2e/pages/safeapps.pages.js +++ b/cypress/e2e/pages/safeapps.pages.js @@ -3,6 +3,10 @@ import * as constants from '../../support/constants' const searchAppInput = 'input[id="search-by-name"]' const appUrlInput = 'input[name="appUrl"]' const closePreviewWindowBtn = 'button[aria-label*="Close"][aria-label*="preview"]' +export const contractMethodIndex = '[name="contractMethodIndex"]' +export const saveToLibraryBtn = 'button[title="Save to Library"]' +export const downloadBatchBtn = 'button[title="Download batch"]' +export const deleteBatchBtn = 'button[title="Delete Batch"]' const addBtnStr = /add/i const noAppsStr = /no Safe Apps found/i @@ -19,13 +23,72 @@ const accessToAddressBookStr = /access to your address book/i const acceptBtnStr = /accept/i const clearAllBtnStr = /clear all/i const allowAllPermissions = /allow all/i +export const enterAddressStr = /enter address or ens name/i +export const addTransactionStr = /add transaction/i +export const createBatchStr = /create batch/i +export const sendBatchStr = /send batch/i +export const transactionDetailsStr = /transaction details/i +export const addOwnerWithThreshold = /add owner with threshold/i +export const enterABIStr = /Enter ABI/i +export const toAddressStr = /to address/i +export const gorValue = /gor value */i +export const dataStr = /data */i +export const clearTransactionListStr = /Clear transaction list?/i +export const confirmClearTransactionListStr = /Yes, clear/i +export const cancelBtnStr = 'Cancel' +export const confirmDeleteBtnStr = 'Yes, delete' +export const backBtnStr = /Back/i +export const simulateBtnStr = /Simulate/i +export const reviewAndConfirmStr = /Review and confirm/i +export const backToTransactionStr = /Back to Transaction Creation/i +export const batchNameStr = /Batch name/i +export const transactionLibraryStr = /Your transaction library/i +export const noSavedBatchesStr = /You don't have any saved batches./i +export const keepProxiABIStr = /Keep Proxy ABI/i +export const selectAllRowsChbxStr = /Select All Rows checkbox/i +export const selectRowChbxStr = /Select Row checkbox/i +export const recipientStr = /recipient/i +export const validRecipientAddressStr = /please enter a valid recipient address/i +export const testAddressValue2 = 'testAddressValue' +export const testBooleanValue = 'testBooleanValue' +export const testBooleanValue1 = '1 testBooleanValue' +export const testBooleanValue2 = '2 testBooleanValue' +export const testBooleanValue3 = '3 testBooleanValue' +export const transfer2AssetsStr = 'Transfer 2 assets' + +export const testTransfer1 = '1 transfer' +export const testTransfer2 = '2 transfer' +export const testNativeTransfer2 = '2 native transfer' +export const testNativeTransfer1 = '1 native transfer' + +export const newValueBool = 'newValue(bool):' +export const ownerAddressStr = 'owner (address)' +export const ownerAddressStr2 = 'owner(address)' +export const thresholdStr = '_threshold (uint256) *' +export const thresholdStr2 = '_threshold(uint256):' const appNotSupportedMsg = "The app doesn't support Safe App functionality" +export const changedPropertiesStr = 'This batch contains some changed properties since you saved or downloaded it' +export const anotherChainStr = 'This batch is from another Chain (1)!' +export const useImplementationABI = 'Use Implementation ABI' +export const addressNotValidStr = 'The address is not valid' +export const transferEverythingStr = 'Transfer everything' +export const noTokensSelectedStr = 'No tokens selected' +export const requiredStr = 'Required' +export const e3eTestStr = 'E2E test' +export const createBtnStr = 'Create' +export const warningStr = 'Warning' +export const transferStr = 'Transfer' +export const successStr = 'Success' +export const failedStr = 'Failed' export const pinWalletConnectStr = /pin walletconnect/i export const transactionBuilderStr = 'Transaction Builder' +export const testAddressValueStr = 'test Address Value' export const logoWalletConnect = /logo.*walletconnect/i export const walletConnectHeadlinePreview = /walletconnect/i +export const newAddressValueStr = 'newValue (address)' +export const newAddressValueStr2 = 'newValue(address)' export const transactiobUilderHeadlinePreview = 'Transaction Builder' export const availableNetworksPreview = 'Available networks' export const connecttextPreview = 'Compose custom contract interactions and batch them into a single transaction' @@ -36,6 +99,8 @@ export const gridItem = 'main .MuiPaper-root > .MuiGrid-item' export const linkNames = { logo: /logo/i, } +export const abi = + '[{{}"inputs":[{{}"internalType":"address","name":"_singleton","type":"address"{}}],"stateMutability":"nonpayable","type":"constructor"{}},{{}"stateMutability":"payable","type":"fallback"{}}]' export const permissionCheckboxes = { camera: 'input[name="camera"]', diff --git a/cypress/e2e/safe-apps/drain_account.spec.cy.js b/cypress/e2e/safe-apps/drain_account.spec.cy.js new file mode 100644 index 0000000000..87d878f307 --- /dev/null +++ b/cypress/e2e/safe-apps/drain_account.spec.cy.js @@ -0,0 +1,93 @@ +import 'cypress-file-upload' +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as safeapps from '../pages/safeapps.pages' + +describe('Drain Account Safe App tests', { defaultCommandTimeout: 12000 }, () => { + const appUrl = constants.drainAccount_url + const iframeSelector = `iframe[id="iframe-${appUrl}"]` + const visitUrl = `/apps/open?safe=${constants.GOERLI_SAFE_APPS_SAFE}&appUrl=${encodeURIComponent(appUrl)}` + + beforeEach(() => { + cy.intercept(`**//v1/chains/5/safes/${constants.GOERLI_SAFE_APPS_SAFE.substring(4)}/balances/**`, { + fixture: 'balances.json', + }) + + cy.clearLocalStorage() + cy.visit(visitUrl) + main.acceptCookies(1) + safeapps.clickOnContinueBtn() + }) + + it('Verify drain can be created [C56627]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.transferEverythingStr).click() + }) + cy.findByRole('button', { name: safeapps.testTransfer1 }) + cy.findByRole('button', { name: safeapps.testNativeTransfer2 }) + }) + + it('Verify partial drain can be created [C56628]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() + getBody().findAllByLabelText(safeapps.selectRowChbxStr).eq(1).click() + getBody().findAllByLabelText(safeapps.selectRowChbxStr).eq(2).click() + getBody().findByLabelText(safeapps.recipientStr).clear().type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.transfer2AssetsStr).click() + }) + cy.findByRole('button', { name: safeapps.testTransfer2 }) + cy.findByRole('button', { name: safeapps.testNativeTransfer1 }) + }) + + it('Verify a drain can be created when a ENS is specified [C56629]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type('goerli-test-safe.eth').wait(2000) + getBody().findAllByText(safeapps.transferEverythingStr).click() + }) + cy.findByRole('button', { name: safeapps.testTransfer1 }) + cy.findByRole('button', { name: safeapps.testNativeTransfer2 }) + }) + + // TODO: Adjust safe - owner + it.skip('Verify when cancelling a drain, previous data is preserved [C56630]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.transferEverythingStr).click() + }) + cy.findByRole('button', { name: safeapps.cancelBtnStr }).click() + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText(safeapps.transferEverythingStr).should('be.visible') + }) + }) + + it('Verify a drain cannot be created with no recipient selected [C56631]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText(safeapps.transferEverythingStr).click() + getBody().findByText(safeapps.validRecipientAddressStr) + }) + }) + + it('Verify a drain cannot be created with invalid recipient selected [C56632]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2.substring(1)) + getBody().findAllByText(safeapps.transferEverythingStr).click() + getBody().findByText(safeapps.validRecipientAddressStr) + }) + }) + + it('Verify a drain cannot be created when no assets are selected [C56633]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() + getBody().findByLabelText(safeapps.recipientStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findAllByText(safeapps.noTokensSelectedStr).should('be.visible') + }) + }) + + it('should not allow to perform a drain when no assets and recipient are selected', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.selectAllRowsChbxStr).click() + getBody().findAllByText(safeapps.noTokensSelectedStr).should('be.visible') + }) + }) +}) diff --git a/cypress/e2e/safe-apps/tx-builder.spec.cy.js b/cypress/e2e/safe-apps/tx-builder.spec.cy.js new file mode 100644 index 0000000000..fd850c112f --- /dev/null +++ b/cypress/e2e/safe-apps/tx-builder.spec.cy.js @@ -0,0 +1,252 @@ +import 'cypress-file-upload' +import * as constants from '../../support/constants' +import * as main from '../pages/main.page' +import * as safeapps from '../pages/safeapps.pages' + +describe('Tx-builder Safe App tests', { defaultCommandTimeout: 20000 }, () => { + const appUrl = constants.TX_Builder_url + const iframeSelector = `iframe[id="iframe-${appUrl}"]` + const visitUrl = `/apps/open?safe=${constants.GOERLI_SAFE_APPS_SAFE}&appUrl=${encodeURIComponent(appUrl)}` + beforeEach(() => { + cy.clearLocalStorage() + cy.visit(visitUrl) + main.acceptCookies(1) + safeapps.clickOnContinueBtn() + }) + + it('Verify a simple batch can be created [C56609]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') + cy.findByRole('button', { name: safeapps.transactionDetailsStr }).click() + cy.findByRole('region').should('exist') + cy.findByText(safeapps.testAddressValueStr).should('exist') + cy.contains(safeapps.newAddressValueStr2).should('exist') + cy.findAllByText(constants.SAFE_APP_ADDRESS_2_SHORT).should('have.length', 1) + }) + + it('Verify a complex batch can be created [C56610]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testBooleanValue }).click() + getBody().findByText(safeapps.addTransactionStr).click() + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testBooleanValue }).click() + getBody().findByText(safeapps.addTransactionStr).click() + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testBooleanValue }).click() + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.findByRole('button', { name: safeapps.testBooleanValue1 }).click() + cy.findByRole('button', { name: safeapps.testBooleanValue2 }).click() + cy.findByRole('button', { name: safeapps.testBooleanValue3 }).click() + cy.findAllByText(safeapps.newValueBool).should('have.length', 3) + cy.findAllByText('True').should('have.length', 3) + }) + + it('Verify a batch can be created using ENS name [C56611]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.ENS_TEST_GOERLI) + getBody().findByRole('button', { name: safeapps.useImplementationABI }).click() + getBody().findByLabelText(safeapps.ownerAddressStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByLabelText(safeapps.thresholdStr).type('1') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.findByRole('button', { name: safeapps.transactionDetailsStr }).click() + cy.findByRole('region').should('exist') + cy.findByText(safeapps.addOwnerWithThreshold).should('exist') + cy.contains(safeapps.ownerAddressStr2).should('exist') + cy.findAllByText(constants.SAFE_APP_ADDRESS_2_SHORT).should('have.length', 1) + cy.findByText(safeapps.thresholdStr2).should('exist') + }) + + it('Verify a batch can be created from an ABI [C56612]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterABIStr).type(safeapps.abi) + getBody().findByLabelText(safeapps.toAddressStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByLabelText(safeapps.gorValue).type('0') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') + cy.findByText(constants.SAFE_APP_ADDRESS_2).should('be.visible') + }) + + it('Verify a batch with custom data can be created [C56613]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().find('.MuiSwitch-root').click() + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findByLabelText(safeapps.gorValue).type('0') + getBody().findByLabelText(safeapps.dataStr).type('0x') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('h4').contains(safeapps.transactionBuilderStr).should('be.visible') + cy.findByText(constants.SAFE_APP_ADDRESS_3).should('be.visible') + }) + + it('Verify a batch can be cancelled [C56614]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByRole('button', { name: safeapps.cancelBtnStr }).click() + getBody().findByText(safeapps.clearTransactionListStr) + getBody().findByRole('button', { name: safeapps.confirmClearTransactionListStr }).click() + getBody().findAllByText('choose a file').should('be.visible') + }) + }) + + it('Verify cancel operation can be reverted [C56615]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByRole('button', { name: safeapps.cancelBtnStr }).click() + getBody().findByText(safeapps.clearTransactionListStr) + getBody().findByRole('button', { name: safeapps.backBtnStr }).click() + getBody().findByText(safeapps.reviewAndConfirmStr).should('be.visible') + }) + }) + + it('Verify it is allowed to go back without removing data and add more transactions to the batch [C56616]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.backToTransactionStr).click() + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS) + getBody().find(safeapps.contractMethodIndex).parent().click() + getBody().findByRole('option', { name: safeapps.testAddressValue2 }).click() + getBody().findByLabelText(safeapps.newAddressValueStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.sendBatchStr).click() + }) + cy.get('p').contains('1').should('be.visible') + cy.get('p').contains('2').should('be.visible') + }) + + it('Verify a batch cannot be created with invalid address [C56617]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findAllByText(safeapps.addressNotValidStr).should('have.css', 'color', 'rgb(244, 67, 54)') + }) + }) + + it('Verify a batch cannot be created without asset amount [C56618]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findAllByText(safeapps.requiredStr).should('have.css', 'color', 'rgb(244, 67, 54)') + }) + }) + + it('Verify a batch cannot be created without method data [C56619]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_2) + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findAllByText(safeapps.requiredStr).should('have.css', 'color', 'rgb(244, 67, 54)') + }) + }) + + it('Verify a batch can be uploaded, saved, downloaded and removed [C56620]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-working-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText('uploaded').wait(300) + getBody().find(safeapps.saveToLibraryBtn).click() + getBody().findByLabelText(safeapps.batchNameStr).type(safeapps.e3eTestStr) + getBody().findAllByText(safeapps.createBtnStr).should('not.be.disabled').click() + getBody().findByText(safeapps.transactionLibraryStr).click() + getBody().find(safeapps.downloadBatchBtn).click() + getBody().find(safeapps.deleteBatchBtn).click() + getBody().findAllByText(safeapps.confirmDeleteBtnStr).should('not.be.disabled').click() + getBody().findByText(safeapps.noSavedBatchesStr).should('be.visible') + getBody().findByText(safeapps.backToTransactionStr).should('be.visible') + }) + cy.readFile('cypress/downloads/E2E test.json').should('exist') + }) + + it('Verify there is notification if uploaded batch is from a different chain [C56621]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-mainnet-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText(safeapps.warningStr).should('be.visible') + getBody().findAllByText(safeapps.anotherChainStr).should('be.visible') + }) + }) + + it('Verify there is error message when a modified batch is uploaded [C56622]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findAllByText('choose a file').attachFile('test-modified-batch.json', { subjectType: 'drag-n-drop' }) + getBody().findAllByText(safeapps.changedPropertiesStr) + getBody().findAllByText('choose a file').should('be.visible') + }) + }) + + it('Verify an invalid batch cannot be uploaded [C56623]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody() + .findAllByText('choose a file') + .attachFile('test-invalid-batch.json', { subjectType: 'drag-n-drop' }) + .findAllByText('choose a file') + .should('be.visible') + }) + }) + + it('Verify an empty batch cannot be uploaded [C56624]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody() + .findAllByText('choose a file') + .attachFile('test-empty-batch.json', { subjectType: 'drag-n-drop' }) + .findAllByText('choose a file') + .should('be.visible') + }) + }) + + it('Verify a valid batch as successful can be simulated [C56625]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SAFE_APP_ADDRESS_3) + getBody().findByLabelText(safeapps.gorValue).type('0') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.simulateBtnStr).click() + getBody().findByText(safeapps.transferStr).should('be.visible') + getBody().findByText(safeapps.successStr).should('be.visible') + }) + }) + + it('Verify an invalid batch as failed can be simulated [C56626]', () => { + cy.enter(iframeSelector).then((getBody) => { + getBody().findByLabelText(safeapps.enterAddressStr).type(constants.TEST_SAFE_2) + getBody().findByText(safeapps.keepProxiABIStr).click() + getBody().findByLabelText(safeapps.gorValue).type('100') + getBody().findByText(safeapps.addTransactionStr).click() + getBody().findByText(safeapps.createBatchStr).click() + getBody().findByText(safeapps.simulateBtnStr).click() + getBody().findByText(safeapps.failedStr).should('be.visible') + }) + }) +}) diff --git a/cypress/e2e/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index 578fc639ee..aae5c455ad 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -215,6 +215,6 @@ describe('Assets tests', () => { cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_4) balances.selectTokenList(balances.tokenListOptions.allTokens) balances.showSendBtn(0) - owner.verifyTooltiptext(owner.disconnectedUserErrorMsg) + owner.verifyTooltiptext(owner.nonOwnerErrorMsg) }) }) diff --git a/cypress/e2e/smoke/balances_pagination.cy.js b/cypress/e2e/smoke/balances_pagination.cy.js index 92539761cd..b1033b9242 100644 --- a/cypress/e2e/smoke/balances_pagination.cy.js +++ b/cypress/e2e/smoke/balances_pagination.cy.js @@ -8,12 +8,10 @@ describe('Balance tests', () => { before(() => { cy.clearLocalStorage() // Open the Safe used for testing - cy.visit(constants.BALANCE_URL + constants.PAGINATION_TEST_SAFE) + cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_6) main.acceptCookies(2) - cy.contains('div', 'Default tokens').click() - cy.wait(100) - cy.contains('div', 'All tokens').click() + balances.selectTokenList(balances.tokenListOptions.allTokens) }) it('Verify a user can change rows per page and navigate to next and previous page [C56073]', () => { diff --git a/cypress/fixtures/balances.json b/cypress/fixtures/balances.json new file mode 100644 index 0000000000..ffcb4343b9 --- /dev/null +++ b/cypress/fixtures/balances.json @@ -0,0 +1,96 @@ +{ + "fiatTotal": "118.36679999999998", + "items": [ + { + "tokenInfo": { + "type": "ERC20", + "address": "0x02ABBDbAaa7b1BB64B5c878f7ac17f8DDa169532", + "decimals": 18, + "symbol": "GNO", + "name": "Gnosis Token", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x02ABBDbAaa7b1BB64B5c878f7ac17f8DDa169532.png" + }, + "balance": "500000000000000000", + "fiatBalance": "65.70705", + "fiatConversion": "131.4141198301599" + }, + { + "tokenInfo": { + "type": "NATIVE_TOKEN", + "address": "0x0000000000000000000000000000000000000000", + "decimals": 18, + "symbol": "GOR", + "name": "Görli Ether", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/chains/5/currency_logo.png" + }, + "balance": "21000000000000000", + "fiatBalance": "35.14308", + "fiatConversion": "1673.48" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", + "decimals": 18, + "symbol": "WETH", + "name": "Wrapped Ether", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6.png" + }, + "balance": "10000000000000000", + "fiatBalance": "16.73480", + "fiatConversion": "1673.48" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "decimals": 18, + "symbol": "UNI", + "name": "Uniswap", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984.png" + }, + "balance": "10000000000000000", + "fiatBalance": "0.78187", + "fiatConversion": "78.18749370339182" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0x3430d04E42a722c5Ae52C5Bffbf1F230C2677600", + "decimals": 18, + "symbol": "COW", + "name": "CoW Protocol Token", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x3430d04E42a722c5Ae52C5Bffbf1F230C2677600.png" + }, + "balance": "10000000000000000000", + "fiatBalance": "0.00000", + "fiatConversion": "0.0" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60", + "decimals": 18, + "symbol": "DAI", + "name": "Dai", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0xdc31Ee1784292379Fbb2964b3B9C4124D8F89C60.png" + }, + "balance": "200000000000000000000", + "fiatBalance": "0.00000", + "fiatConversion": "0.00000000002294647997868982" + }, + { + "tokenInfo": { + "type": "ERC20", + "address": "0x1B809925ba90c541d895D19f0b7D70eE281a987F", + "decimals": 0, + "symbol": "VanityTRX.org", + "name": "VanityTRX.org", + "logoUri": "https://safe-transaction-assets.staging.5afe.dev/tokens/logos/0x1B809925ba90c541d895D19f0b7D70eE281a987F.png" + }, + "balance": "888888", + "fiatBalance": "0.00000", + "fiatConversion": "0.0" + } + ] +} diff --git a/cypress/fixtures/test-empty-batch.json b/cypress/fixtures/test-empty-batch.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/cypress/fixtures/test-empty-batch.json @@ -0,0 +1 @@ +{} diff --git a/cypress/fixtures/test-invalid-batch.json b/cypress/fixtures/test-invalid-batch.json new file mode 100644 index 0000000000..51ed055cf0 --- /dev/null +++ b/cypress/fixtures/test-invalid-batch.json @@ -0,0 +1,3 @@ +{ + "test": "I am not a valid batch" +} diff --git a/cypress/fixtures/test-mainnet-batch.json b/cypress/fixtures/test-mainnet-batch.json new file mode 100644 index 0000000000..d623b2678a --- /dev/null +++ b/cypress/fixtures/test-mainnet-batch.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1671532788473, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.13.1", + "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", + "createdFromOwnerAddress": "", + "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" + }, + "transactions": [ + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], + "name": "testBooleanValue", + "payable": false + }, + "contractInputsValues": { "newValue": "true" } + }, + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], + "name": "testAddressValue", + "payable": false + }, + "contractInputsValues": { "newValue": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4" } + } + ] +} diff --git a/cypress/fixtures/test-modified-batch.json b/cypress/fixtures/test-modified-batch.json new file mode 100644 index 0000000000..fc9baf3047 --- /dev/null +++ b/cypress/fixtures/test-modified-batch.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1671532788473, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.13.1", + "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", + "createdFromOwnerAddress": "", + "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" + }, + "transactions": [ + { + "to": "", + "value": "", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], + "name": "testBooleanValue", + "payable": false + }, + "contractInputsValues": { "newValue": "true" } + }, + { + "to": "", + "value": "", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], + "name": "testAddressValue", + "payable": false + }, + "contractInputsValues": { "newValue": "" } + } + ] +} diff --git a/cypress/fixtures/test-working-batch.json b/cypress/fixtures/test-working-batch.json new file mode 100644 index 0000000000..a77bf55c3d --- /dev/null +++ b/cypress/fixtures/test-working-batch.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "chainId": "5", + "createdAt": 1671532788473, + "meta": { + "name": "Transactions Batch", + "description": "", + "txBuilderVersion": "1.13.1", + "createdFromSafeAddress": "0xE96C43C54B08eC528e9e815fC3D02Ea94A320505", + "createdFromOwnerAddress": "", + "checksum": "0x783b24b06f925df195ac0e0103507caf6520cff278555c11e9b8edb43bc2a196" + }, + "transactions": [ + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "bool", "name": "newValue", "type": "bool" }], + "name": "testBooleanValue", + "payable": false + }, + "contractInputsValues": { "newValue": "true" } + }, + { + "to": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4", + "value": "0", + "data": null, + "contractMethod": { + "inputs": [{ "internalType": "address", "name": "newValue", "type": "address" }], + "name": "testAddressValue", + "payable": false + }, + "contractInputsValues": { "newValue": "0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4" } + } + ] +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ed51e2fe5e..b8c8e64fee 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -57,3 +57,157 @@ const waitForSelector = (item, options = {}) => { } Cypress.Commands.add('waitForSelector', waitForSelector) + +const DEFAULT_OPTS = { + log: true, + timeout: 30000, +} + +const DEFAULT_IFRAME_SELECTOR = 'iframe' + +function sleep(timeout) { + return new Promise((resolve) => setTimeout(resolve, timeout)) +} + +// This command checks that an iframe has loaded onto the page +// - This will verify that the iframe is loaded to any page other than 'about:blank' +// cy.frameLoaded() + +// - This will verify that the iframe is loaded to any url containing the given path part +// cy.frameLoaded({ url: 'https://google.com' }) +// cy.frameLoaded({ url: '/join' }) +// cy.frameLoaded({ url: '?some=query' }) +// cy.frameLoaded({ url: '#/hash/path' }) + +// - You can also give it a selector to check that a specific iframe has loaded +// cy.frameLoaded('#my-frame') +// cy.frameLoaded('#my-frame', { url: '/join' }) +Cypress.Commands.add('frameLoaded', (selector, opts) => { + if (selector === undefined) { + selector = DEFAULT_IFRAME_SELECTOR + } else if (typeof selector === 'object') { + opts = selector + selector = DEFAULT_IFRAME_SELECTOR + } + + const fullOpts = { + ...DEFAULT_OPTS, + ...opts, + } + const log = fullOpts.log + ? Cypress.log({ + name: 'frame loaded', + displayName: 'frame loaded', + message: [selector], + }).snapshot() + : null + return cy.get(selector, { log: false }).then({ timeout: fullOpts.timeout }, async ($frame) => { + log?.set('$el', $frame) + if ($frame.length !== 1) { + throw new Error( + `cypress-iframe commands can only be applied to exactly one iframe at a time. Instead found ${$frame.length}`, + ) + } + + const contentWindow = $frame.prop('contentWindow') + const hasNavigated = fullOpts.url + ? () => + typeof fullOpts.url === 'string' + ? contentWindow.location.toString().includes(fullOpts.url) + : fullOpts.url?.test(contentWindow.location.toString()) + : () => contentWindow.location.toString() !== 'about:blank' + + while (!hasNavigated()) { + await sleep(100) + } + + if (contentWindow.document.readyState === 'complete') { + return $frame + } + + const loadLog = Cypress.log({ + name: 'Frame Load', + message: [contentWindow.location.toString()], + event: true, + }).snapshot() + await new Promise((resolve) => { + Cypress.$(contentWindow).on('load', resolve) + }) + loadLog.end() + log?.finish() + return $frame + }) +}) + +// This will cause subsequent commands to be executed inside of the given iframe +// - This will verify that the iframe is loaded to any page other than 'about:blank' +// cy.iframe().find('.some-button').should('be.visible').click() +// cy.iframe().contains('Some hidden element').should('not.be.visible') +// cy.find('#outside-iframe').click() // this will be executed outside the iframe + +// - You can also give it a selector to find elements inside of a specific iframe +// cy.iframe('#my-frame').find('.some-button').should('be.visible').click() +// cy.iframe('#my-second-frame').contains('Some hidden element').should('not.be.visible') +Cypress.Commands.add('iframe', (selector, opts) => { + if (selector === undefined) { + selector = DEFAULT_IFRAME_SELECTOR + } else if (typeof selector === 'object') { + opts = selector + selector = DEFAULT_IFRAME_SELECTOR + } + + const fullOpts = { + ...DEFAULT_OPTS, + ...opts, + } + const log = fullOpts.log + ? Cypress.log({ + name: 'iframe', + displayName: 'iframe', + message: [selector], + }).snapshot() + : null + return cy.frameLoaded(selector, { ...fullOpts, log: false }).then(($frame) => { + log?.set('$el', $frame).end() + const contentWindow = $frame.prop('contentWindow') + return Cypress.$(contentWindow.document.body) + }) +}) + +// This can be used to execute a group of commands within an iframe +// - This will verify that the iframe is loaded to any page other than 'about:blank' +// cy.enter().then(getBody => { +// getBody().find('.some-button').should('be.visible').click() +// getBody().contains('Some hidden element').should('not.be.visible') +// }) +// - You can also give it a selector to find elements inside of a specific iframe +// cy.enter('#my-iframe').then(getBody => { +// getBody().find('.some-button').should('be.visible').click() +// getBody().contains('Some hidden element').should('not.be.visible') +// }) +Cypress.Commands.add('enter', (selector, opts) => { + if (selector === undefined) { + selector = DEFAULT_IFRAME_SELECTOR + } else if (typeof selector === 'object') { + opts = selector + selector = DEFAULT_IFRAME_SELECTOR + } + + const fullOpts = { + ...DEFAULT_OPTS, + ...opts, + } + + const log = fullOpts.log + ? Cypress.log({ + name: 'enter', + displayName: 'enter', + message: [selector], + }).snapshot() + : null + + return cy.iframe(selector, { ...fullOpts, log: false }).then(($body) => { + log?.set('$el', $body).end() + return () => cy.wrap($body, { log: false }) + }) +}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 8aff12d459..6daeacfc92 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -1,5 +1,6 @@ import { LS_NAMESPACE } from '../../src/config/constants' export const RECIPIENT_ADDRESS = '0x6a5602335a878ADDCa4BF63a050E34946B56B5bC' +export const GOERLI_SAFE_APPS_SAFE = 'gor:0x168ca275d1103cb0a30980813140053c7566932F' export const GOERLI_TEST_SAFE = 'gor:0x97d314157727D517A706B5D08507A1f9B44AaaE9' // SEPOLIA_TEST_SAFE_1 should always have no transactions, tokens and NFTs export const SEPOLIA_TEST_SAFE_1 = 'sep:0xBb26E3717172d5000F87DeFd391994f789D80aEB' @@ -8,15 +9,21 @@ export const SEPOLIA_TEST_SAFE_2 = 'sep:0x33C4AA5729D91FfB3B87AEf8a324bb6304Fb90 export const SEPOLIA_TEST_SAFE_3 = 'sep:0x6E834E9D04ad6b26e1525dE1a37BFd9b215f40B7' export const SEPOLIA_TEST_SAFE_4 = 'sep:0x03042B890b99552b60A073F808100517fb148F60' export const SEPOLIA_TEST_SAFE_5 = 'sep:0xBd69b0a9DC90eB6F9bAc3E4a5875f437348b6415' +export const SEPOLIA_TEST_SAFE_6 = 'sep:0x6d0b6F96f665Bb4490f9ddb2e450Da2f7e546dC1' export const GNO_TEST_SAFE = 'gno:0xB8d760a90a5ed54D3c2b3EFC231277e99188642A' export const PAGINATION_TEST_SAFE = 'gor:0x850493a15914aAC05a821A3FAb973b4598889A7b' export const TEST_SAFE = 'gor:0x04f8b1EA3cBB315b87ced0E32deb5a43cC151a91' export const EOA = '0x03042B890b99552b60A073F808100517fb148F60' +export const SAFE_APP_ADDRESS = '0x51A099ac1BF46D471110AA8974024Bfe518Fd6C4' +export const SAFE_APP_ADDRESS_2 = '0x49d4450977E2c95362C13D3a31a09311E0Ea26A6' +export const SAFE_APP_ADDRESS_3 = '0xc6b82bA149CFA113f8f48d5E3b1F78e933e16DfD' +export const SAFE_APP_ADDRESS_2_SHORT = '0x49d4...26A6' export const DEFAULT_OWNER_ADDRESS = '0xC16Db0251654C0a72E91B190d81eAD367d2C6fED' export const SEPOLIA_OWNER_2 = '0x96D4c6fFC338912322813a77655fCC926b9A5aC5' export const TEST_SAFE_2 = 'gor:0xE96C43C54B08eC528e9e815fC3D02Ea94A320505' export const SIDEBAR_ADDRESS = '0x04f8...1a91' export const ENS_TEST_SEPOLIA = 'testenssepolia.eth' +export const ENS_TEST_GOERLI = 'goerli-safe-test.eth' export const ENS_TEST_SEPOLIA_INVALID = 'ivladitestenssepolia.eth' export const BROWSER_PERMISSIONS_KEY = `${LS_NAMESPACE}SafeApps__browserPermissions` @@ -28,6 +35,8 @@ export const goerlySafeName = /g(ö|oe)rli-safe/ export const sepoliaSafeName = 'sepolia-safe' export const goerliToken = /G(ö|oe)rli Ether/ +export const TX_Builder_url = 'https://safe-apps.dev.5afe.dev/tx-builder' +export const drainAccount_url = 'https://safe-apps.dev.5afe.dev/drain-safe' export const testAppUrl = 'https://safe-test-app.com' export const addressBookUrl = '/address-book?safe=' export const BALANCE_URL = '/balances?safe=' From af972440891283ea97390f0aea48602cdb1ad666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ux=C3=ADo?= Date: Wed, 8 Nov 2023 18:13:16 +0100 Subject: [PATCH 04/18] Chore: Optimize docker image (#2771) - Reduce size by using multistage builds - Run `yarn build` on the build process and not when the docker image starts (it can take a few minutes to complete) --- .dockerignore | 6 +++++- Dockerfile | 35 ++++++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.dockerignore b/.dockerignore index 355611305d..d659b8076c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,10 +2,14 @@ Dockerfile .dockerignore node_modules npm-debug.log -README.md .next .git coverage .DS_Store .idea dist + +build/ +coverage/ +cypress/ +out/ diff --git a/Dockerfile b/Dockerfile index 82391964d5..9e8cba5245 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,34 @@ -FROM node:18-alpine +FROM node:18-alpine AS base +ENV NEXT_TELEMETRY_DISABLED 1 + +FROM base AS builder + RUN apk add --no-cache libc6-compat git python3 py3-pip make g++ libusb-dev eudev-dev linux-headers WORKDIR /app + +# Install dependencies +COPY package.json yarn.lock* ./ +RUN yarn --frozen-lockfile COPY . . +RUN yarn run after-install -# install deps -RUN yarn install --frozen-lockfile -RUN yarn after-install +RUN yarn build + +# Production image +FROM base AS runner +WORKDIR /app ENV NODE_ENV production +ENV REVERSE_PROXY_UI_PORT 8080 -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry during the build. -ENV NEXT_TELEMETRY_DISABLED 1 +RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs +COPY --from=builder /app/out ./out + +# Set the correct permission for prerender cache +RUN mkdir .next && chown nextjs:nodejs .next -EXPOSE 3000 +USER nextjs -ENV PORT 3000 +EXPOSE ${REVERSE_PROXY_UI_PORT} -CMD ["yarn", "static-serve"] +CMD npx -y serve out -p ${REVERSE_PROXY_UI_PORT} From e72f2223da693e63168c076a5358ecd7746f5c76 Mon Sep 17 00:00:00 2001 From: Usame Algan <5880855+usame-algan@users.noreply.github.com> Date: Thu, 9 Nov 2023 09:02:10 +0100 Subject: [PATCH 05/18] fix: Remove WC onboarding tooltip (#2746) * fix: Remove WC onboarding tooltip * fix: Onboard Safe logo size * fix: Update e2e tests acceptCookies call * fix: Failing e2e test --- cypress/e2e/safe-apps/apps_list.cy.js | 2 +- .../e2e/safe-apps/browser_permissions.cy.js | 2 +- .../e2e/safe-apps/drain_account.spec.cy.js | 2 +- cypress/e2e/safe-apps/safe_permissions.cy.js | 4 ++-- cypress/e2e/safe-apps/tx-builder.spec.cy.js | 4 ++-- cypress/e2e/smoke/add_owner.cy.js | 2 +- cypress/e2e/smoke/address_book.cy.js | 2 +- cypress/e2e/smoke/assets.cy.js | 2 +- cypress/e2e/smoke/balances.cy.js | 2 +- cypress/e2e/smoke/balances_pagination.cy.js | 2 +- cypress/e2e/smoke/batch_tx.cy.js | 2 +- cypress/e2e/smoke/create_tx.cy.js | 2 +- cypress/e2e/smoke/dashboard.cy.js | 2 +- cypress/e2e/smoke/import_export_data.cy.js | 2 +- cypress/e2e/smoke/nfts.cy.js | 2 +- cypress/e2e/smoke/tx_history.cy.js | 2 +- public/images/logo-round.svg | 2 +- .../walletconnect/WcHeaderWidget/index.tsx | 24 +++++++------------ 18 files changed, 27 insertions(+), 35 deletions(-) diff --git a/cypress/e2e/safe-apps/apps_list.cy.js b/cypress/e2e/safe-apps/apps_list.cy.js index 4081b813cf..9265117bcd 100644 --- a/cypress/e2e/safe-apps/apps_list.cy.js +++ b/cypress/e2e/safe-apps/apps_list.cy.js @@ -9,7 +9,7 @@ describe('Safe Apps tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.SEPOLIA_TEST_SAFE_4 + constants.appsUrl, { failOnStatusCode: false }) - main.acceptCookies(1) + main.acceptCookies() }) it('Verify app list can be filtered by app name [C56130]', () => { diff --git a/cypress/e2e/safe-apps/browser_permissions.cy.js b/cypress/e2e/safe-apps/browser_permissions.cy.js index da34de03ab..916604d9a6 100644 --- a/cypress/e2e/safe-apps/browser_permissions.cy.js +++ b/cypress/e2e/safe-apps/browser_permissions.cy.js @@ -15,7 +15,7 @@ describe('Browser permissions tests', () => { }) }) cy.visitSafeApp(`${constants.testAppUrl}/app`) - main.acceptCookies(1) + main.acceptCookies() }) it('Verify a permissions slide to the user is displayed [C56137]', () => { diff --git a/cypress/e2e/safe-apps/drain_account.spec.cy.js b/cypress/e2e/safe-apps/drain_account.spec.cy.js index 87d878f307..1536de692f 100644 --- a/cypress/e2e/safe-apps/drain_account.spec.cy.js +++ b/cypress/e2e/safe-apps/drain_account.spec.cy.js @@ -15,7 +15,7 @@ describe('Drain Account Safe App tests', { defaultCommandTimeout: 12000 }, () => cy.clearLocalStorage() cy.visit(visitUrl) - main.acceptCookies(1) + main.acceptCookies() safeapps.clickOnContinueBtn() }) diff --git a/cypress/e2e/safe-apps/safe_permissions.cy.js b/cypress/e2e/safe-apps/safe_permissions.cy.js index 771beee30d..3b2b6fa6f8 100644 --- a/cypress/e2e/safe-apps/safe_permissions.cy.js +++ b/cypress/e2e/safe-apps/safe_permissions.cy.js @@ -17,7 +17,7 @@ describe('Safe permissions system tests', () => { it('Verify that requesting permissions with wallet_requestPermissions shows the permissions prompt and return the permissions on accept [C56150]', () => { cy.visitSafeApp(constants.testAppUrl + constants.requestPermissionsUrl) - main.acceptCookies(1) + main.acceptCookies() safeapps.clickOnContinueBtn() safeapps.verifyWarningDefaultAppMsgIsDisplayed() safeapps.clickOnContinueBtn() @@ -56,7 +56,7 @@ describe('Safe permissions system tests', () => { }) cy.visitSafeApp(constants.testAppUrl + constants.getPermissionsUrl) - main.acceptCookies(1) + main.acceptCookies() safeapps.clickOnContinueBtn() safeapps.verifyWarningDefaultAppMsgIsDisplayed() safeapps.clickOnContinueBtn() diff --git a/cypress/e2e/safe-apps/tx-builder.spec.cy.js b/cypress/e2e/safe-apps/tx-builder.spec.cy.js index fd850c112f..e2399d03e4 100644 --- a/cypress/e2e/safe-apps/tx-builder.spec.cy.js +++ b/cypress/e2e/safe-apps/tx-builder.spec.cy.js @@ -10,7 +10,7 @@ describe('Tx-builder Safe App tests', { defaultCommandTimeout: 20000 }, () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(visitUrl) - main.acceptCookies(1) + main.acceptCookies() safeapps.clickOnContinueBtn() }) @@ -146,7 +146,7 @@ describe('Tx-builder Safe App tests', { defaultCommandTimeout: 20000 }, () => { getBody().findByText(safeapps.createBatchStr).click() getBody().findByText(safeapps.sendBatchStr).click() }) - cy.get('p').contains('1').should('be.visible') + cy.get('p').contains('1').should('exist') cy.get('p').contains('2').should('be.visible') }) diff --git a/cypress/e2e/smoke/add_owner.cy.js b/cypress/e2e/smoke/add_owner.cy.js index a36635750c..e19dbca96b 100644 --- a/cypress/e2e/smoke/add_owner.cy.js +++ b/cypress/e2e/smoke/add_owner.cy.js @@ -7,7 +7,7 @@ describe('Add Owners tests', () => { beforeEach(() => { cy.visit(constants.setupUrl + constants.SEPOLIA_TEST_SAFE_1) cy.clearLocalStorage() - main.acceptCookies(1) + main.acceptCookies() cy.contains(owner.safeAccountNonceStr, { timeout: 10000 }) }) diff --git a/cypress/e2e/smoke/address_book.cy.js b/cypress/e2e/smoke/address_book.cy.js index 3a5af18932..d14ca5e2b4 100644 --- a/cypress/e2e/smoke/address_book.cy.js +++ b/cypress/e2e/smoke/address_book.cy.js @@ -12,7 +12,7 @@ describe('Address book tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.addressBookUrl + constants.SEPOLIA_TEST_SAFE_1) - main.acceptCookies(1) + main.acceptCookies() }) it('Verify entry can be added [C56061]', () => { diff --git a/cypress/e2e/smoke/assets.cy.js b/cypress/e2e/smoke/assets.cy.js index aae5c455ad..f056ce3bae 100644 --- a/cypress/e2e/smoke/assets.cy.js +++ b/cypress/e2e/smoke/assets.cy.js @@ -13,7 +13,7 @@ describe('Assets tests', () => { beforeEach(() => { cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) cy.clearLocalStorage() - main.acceptCookies(2) + main.acceptCookies() }) it('Verify that the token tab is selected by default and the table is visible [C56039]', () => { diff --git a/cypress/e2e/smoke/balances.cy.js b/cypress/e2e/smoke/balances.cy.js index df8e1c3681..7d5a0bb7b9 100644 --- a/cypress/e2e/smoke/balances.cy.js +++ b/cypress/e2e/smoke/balances.cy.js @@ -14,7 +14,7 @@ describe('Balance tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) - main.acceptCookies(2) + main.acceptCookies() cy.contains('Assets') cy.get(balances.balanceSingleRow).should('have.length.lessThan', ASSETS_LENGTH) balances.selectTokenList(balances.tokenListOptions.allTokens) diff --git a/cypress/e2e/smoke/balances_pagination.cy.js b/cypress/e2e/smoke/balances_pagination.cy.js index b1033b9242..27703f9c2b 100644 --- a/cypress/e2e/smoke/balances_pagination.cy.js +++ b/cypress/e2e/smoke/balances_pagination.cy.js @@ -9,7 +9,7 @@ describe('Balance tests', () => { cy.clearLocalStorage() // Open the Safe used for testing cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_6) - main.acceptCookies(2) + main.acceptCookies() balances.selectTokenList(balances.tokenListOptions.allTokens) }) diff --git a/cypress/e2e/smoke/batch_tx.cy.js b/cypress/e2e/smoke/batch_tx.cy.js index 507b2b3ecd..347d723e77 100644 --- a/cypress/e2e/smoke/batch_tx.cy.js +++ b/cypress/e2e/smoke/batch_tx.cy.js @@ -10,7 +10,7 @@ describe('Batch transaction tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) - main.acceptCookies(2) + main.acceptCookies() }) it('Verify empty batch list can be opened [C56082]', () => { diff --git a/cypress/e2e/smoke/create_tx.cy.js b/cypress/e2e/smoke/create_tx.cy.js index b1a5e1ac1a..a96db26acc 100644 --- a/cypress/e2e/smoke/create_tx.cy.js +++ b/cypress/e2e/smoke/create_tx.cy.js @@ -9,7 +9,7 @@ describe('Create transactions tests', () => { before(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) - main.acceptCookies(2) + main.acceptCookies() }) it('Verify a new send token transaction can be created [C56104]', () => { diff --git a/cypress/e2e/smoke/dashboard.cy.js b/cypress/e2e/smoke/dashboard.cy.js index ad0ca6d0ee..c69c918df1 100644 --- a/cypress/e2e/smoke/dashboard.cy.js +++ b/cypress/e2e/smoke/dashboard.cy.js @@ -6,7 +6,7 @@ describe('Dashboard tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) - main.acceptCookies(2) + main.acceptCookies() main.clickOnSideMenuItem(constants.mainSideMenuOptions.home) dashboard.verifyConnectTransactStrIsVisible() }) diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/smoke/import_export_data.cy.js index de7e8f3881..140b5d9dc1 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/smoke/import_export_data.cy.js @@ -24,7 +24,7 @@ describe('Import Export Data tests', () => { }) it('Verify address book imported data [C56112]', () => { - main.acceptCookies(1) + main.acceptCookies() file.clickOnAddressBookBtn() file.verifyImportedAddressBookData() }) diff --git a/cypress/e2e/smoke/nfts.cy.js b/cypress/e2e/smoke/nfts.cy.js index e86f1d671a..b6015e592a 100644 --- a/cypress/e2e/smoke/nfts.cy.js +++ b/cypress/e2e/smoke/nfts.cy.js @@ -10,7 +10,7 @@ describe('NFTs tests', () => { beforeEach(() => { cy.clearLocalStorage() cy.visit(constants.BALANCE_URL + constants.SEPOLIA_TEST_SAFE_5) - main.acceptCookies(2) + main.acceptCookies() nfts.clickOnNftsTab() }) diff --git a/cypress/e2e/smoke/tx_history.cy.js b/cypress/e2e/smoke/tx_history.cy.js index 42fdbeeb44..4ad1e5b929 100644 --- a/cypress/e2e/smoke/tx_history.cy.js +++ b/cypress/e2e/smoke/tx_history.cy.js @@ -17,7 +17,7 @@ describe('Transaction history tests', () => { // So that tests that rely on this feature don't randomly fail cy.window().then((win) => win.localStorage.setItem('SAFE_v2__AB_human-readable', true)) - main.acceptCookies(1) + main.acceptCookies() }) it('Verify October 29th transactions are displayed [C56128]', () => { diff --git a/public/images/logo-round.svg b/public/images/logo-round.svg index 7782f914a7..dd8af7d0ca 100644 --- a/public/images/logo-round.svg +++ b/public/images/logo-round.svg @@ -1,4 +1,4 @@ - + diff --git a/src/components/walletconnect/WcHeaderWidget/index.tsx b/src/components/walletconnect/WcHeaderWidget/index.tsx index fa95cab249..51382b8ad6 100644 --- a/src/components/walletconnect/WcHeaderWidget/index.tsx +++ b/src/components/walletconnect/WcHeaderWidget/index.tsx @@ -2,8 +2,6 @@ import { type ReactNode, useRef } from 'react' import type { SessionTypes } from '@walletconnect/types' import Popup from '@/components/common/Popup' import WcIcon from './WcIcon' -import { OnboardingTooltip } from '@/components/common/OnboardingTooltip' -import useSafeInfo from '@/hooks/useSafeInfo' type WcHeaderWidgetProps = { children: ReactNode @@ -14,25 +12,19 @@ type WcHeaderWidgetProps = { onClose: () => void } -const TOOLTIP_TEXT = 'Connect Safe{Wallet} to any dApp with WalletConnect' -const TOOLTIP_ID = 'native_wc_onboarding' - const WcHeaderWidget = ({ sessions, ...props }: WcHeaderWidgetProps) => { const iconRef = useRef(null) - const { safeLoaded } = useSafeInfo() return ( <> - -
- -
- +
+ +
{props.children} From b2ba82dee6ebbe8cf6114caf6b188d68376007ea Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:44:51 +0100 Subject: [PATCH 06/18] Fix: redirect /wc URL (#2747) * Fix: redirect /wc URL * Use raw URL query * Fix tests * Rm raw query parsing * Fix: unblock Base and Arbitrum bridges in WC --- .../walletconnect/WcInput/index.tsx | 2 + src/config/routes.ts | 3 +- src/pages/wc.tsx | 40 +++++++++++++++++++ src/services/walletconnect/constants.ts | 4 +- .../useWalletConnectSearchParamUri.ts | 9 +---- 5 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 src/pages/wc.tsx diff --git a/src/components/walletconnect/WcInput/index.tsx b/src/components/walletconnect/WcInput/index.tsx index fcfa32a919..125695708d 100644 --- a/src/components/walletconnect/WcInput/index.tsx +++ b/src/components/walletconnect/WcInput/index.tsx @@ -79,7 +79,9 @@ const WcInput = ({ uri }: { uri: string }) => { error={!!error} label={error ? error.message : 'Pairing code'} placeholder="wc:" + spellCheck={false} InputProps={{ + autoComplete: 'off', endAdornment: isClipboardSupported() ? undefined : ( diff --git a/src/config/routes.ts b/src/config/routes.ts index 6090dbb7ad..27402a440f 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,7 +1,7 @@ export const AppRoutes = { '404': '/404', - _offline: '/_offline', welcome: '/welcome', + wc: '/wc', terms: '/terms', privacy: '/privacy', licenses: '/licenses', @@ -11,6 +11,7 @@ export const AppRoutes = { cookie: '/cookie', addressBook: '/address-book', addOwner: '/addOwner', + _offline: '/_offline', apps: { open: '/apps/open', index: '/apps', diff --git a/src/pages/wc.tsx b/src/pages/wc.tsx new file mode 100644 index 0000000000..02c41159db --- /dev/null +++ b/src/pages/wc.tsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react' +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +import useLastSafe from '@/hooks/useLastSafe' +import { AppRoutes } from '@/config/routes' +import { WC_URI_SEARCH_PARAM } from '@/services/walletconnect/useWalletConnectSearchParamUri' + +const WcPage: NextPage = () => { + const router = useRouter() + const lastSafe = useLastSafe() + + useEffect(() => { + if (!router.isReady || router.pathname !== AppRoutes.wc) { + return + } + + const { uri } = router.query + + router.replace( + lastSafe + ? { + pathname: AppRoutes.home, + query: { + safe: lastSafe, + [WC_URI_SEARCH_PARAM]: uri, + }, + } + : { + pathname: AppRoutes.welcome, + query: { + [WC_URI_SEARCH_PARAM]: uri, + }, + }, + ) + }, [router, lastSafe]) + + return <> +} + +export default WcPage diff --git a/src/services/walletconnect/constants.ts b/src/services/walletconnect/constants.ts index 435b964832..2206a0069c 100644 --- a/src/services/walletconnect/constants.ts +++ b/src/services/walletconnect/constants.ts @@ -36,8 +36,6 @@ export const EIP155 = 'eip155' as const // Bridges enforcing same address on destination chains export const BlockedBridges = [ 'app.chainport.io', - 'bridge.arbitrum.io', - 'bridge.base.org', 'cbridge.celer.network', 'www.orbiter.finance', 'zksync-era.l2scan.co', @@ -66,6 +64,8 @@ export const BlockedBridges = [ export const WarnedBridges = [ 'across.to', // doesn't send their URL in the session proposal 'app.allbridge.io', + 'bridge.arbitrum.io', + 'bridge.base.org', 'core.allbridge.io', 'bungee.exchange', 'www.carrier.so', diff --git a/src/services/walletconnect/useWalletConnectSearchParamUri.ts b/src/services/walletconnect/useWalletConnectSearchParamUri.ts index d21a1bc14a..41ed350120 100644 --- a/src/services/walletconnect/useWalletConnectSearchParamUri.ts +++ b/src/services/walletconnect/useWalletConnectSearchParamUri.ts @@ -1,16 +1,11 @@ import { useRouter } from 'next/router' import { useCallback } from 'react' -import { isPairingUri } from './utils' - -const WC_URI_SEARCH_PARAM = 'wc' +export const WC_URI_SEARCH_PARAM = 'wc' export function useWalletConnectSearchParamUri(): [string | null, (wcUri: string | null) => void] { const router = useRouter() - - const wcUriQuery = router.query[WC_URI_SEARCH_PARAM] - const value = wcUriQuery ? (Array.isArray(wcUriQuery) ? wcUriQuery[0] : wcUriQuery) : null - const wcUri = value && isPairingUri(value) ? value : null + const wcUri = (router.query[WC_URI_SEARCH_PARAM] || '').toString() || null const setWcUri = useCallback( (wcUri: string | null) => { From 798e998b4c08cfd00c6a1af9fffd8290e9f99f5c Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Thu, 9 Nov 2023 16:06:54 +0100 Subject: [PATCH 07/18] Epic Seedless-onboarding (#2534) - Adds option to create a social signer via google - Adds Settings page for setting up MFA via password recovery for Social signer - Redesigns Welcome Page --------- Co-authored-by: Usame Algan Co-authored-by: Usame Algan <5880855+usame-algan@users.noreply.github.com> Co-authored-by: Michael <30682308+mike10ca@users.noreply.github.com> Co-authored-by: katspaugh <381895+katspaugh@users.noreply.github.com> --- .env.example | 7 +- .github/workflows/build/action.yml | 2 + cypress/e2e/pages/create_wallet.pages.js | 25 +- cypress/e2e/pages/import_export.pages.js | 6 +- cypress/e2e/pages/load_safe.pages.js | 4 +- cypress/e2e/pages/owners.pages.js | 7 +- cypress/e2e/smoke/create_safe_simple.cy.js | 42 +- cypress/e2e/smoke/import_export_data.cy.js | 6 +- cypress/support/constants.js | 1 + jest.config.cjs | 4 +- next.config.mjs | 2 +- package.json | 5 +- public/images/common/bar-chart.svg | 5 + public/images/common/check-filled.svg | 3 + public/images/common/lock-small.svg | 4 + public/images/common/lock-warning.svg | 6 + public/images/common/lock.svg | 3 + public/images/common/shield-off.svg | 12 + public/images/common/shield.svg | 3 + public/images/logo-text.svg | 9 + public/images/welcome/logo-google.svg | 6 + .../common/ConnectWallet/AccountCenter.tsx | 89 +- .../common/ConnectWallet/ConnectionCenter.tsx | 79 ++ .../common/ConnectWallet/WalletDetails.tsx | 30 + .../__tests__/AccountCenter.test.tsx | 35 + .../__tests__/ConnectionCenter.test.tsx | 18 + src/components/common/ConnectWallet/index.tsx | 5 +- .../common/ConnectWallet/styles.module.css | 52 +- .../common/ConnectWallet/useConnectWallet.ts | 14 +- src/components/common/ErrorBoundary/index.tsx | 2 +- .../EthHashInfo/SrcEthHashInfo/index.tsx | 2 +- src/components/common/Footer/index.tsx | 2 +- src/components/common/Header/index.tsx | 2 +- .../common/NetworkSelector/index.tsx | 28 +- .../common/NetworkSelector/styles.module.css | 4 + .../common/PageHeader/styles.module.css | 2 +- src/components/common/PageLayout/index.tsx | 1 - .../common/SafeLoadingError/index.tsx | 2 +- .../common/SocialLoginInfo/index.tsx | 55 ++ .../common/SocialLoginInfo/styles.module.css | 35 + .../common/SocialSigner/PasswordRecovery.tsx | 114 +++ .../__tests__/PasswordRecovery.test.tsx | 48 + .../__tests__/SocialSignerLogin.test.tsx | 201 +++++ src/components/common/SocialSigner/index.tsx | 185 ++++ .../common/SocialSigner/styles.module.css | 24 + .../common/WalletInfo/index.test.tsx | 182 ++++ src/components/common/WalletInfo/index.tsx | 137 ++- .../common/WalletInfo/styles.module.css | 44 +- .../common/WalletOverview/index.tsx | 56 ++ .../common/WalletOverview/styles.module.css | 38 + .../common/icons/KeyholeIcon/index.tsx | 31 +- .../new-safe/create/OverviewWidget/index.tsx | 4 +- .../__tests__/useSyncSafeCreationStep.test.ts | 23 +- src/components/new-safe/create/index.tsx | 23 +- .../create/steps/ConnectWalletStep/index.tsx | 40 - .../create/steps/ReviewStep/index.test.tsx | 42 + .../create/steps/ReviewStep/index.tsx | 142 ++- .../create/steps/SetNameStep/index.tsx | 12 +- .../create/steps/StatusStep/index.tsx | 2 +- .../create/useSyncSafeCreationStep.ts | 14 +- src/components/new-safe/load/index.tsx | 2 +- .../PushNotificationsBanner/index.tsx | 11 +- .../ExportMPCAccountModal.tsx | 147 ++++ .../SocialSignerExport/index.tsx | 47 + .../SocialSignerExport/styles.module.css | 10 + .../SocialSignerMFA/PasswordInput.tsx | 44 + .../SocialSignerMFA/index.test.tsx | 45 + .../SecurityLogin/SocialSignerMFA/index.tsx | 298 +++++++ .../SocialSignerMFA/styles.module.css | 76 ++ .../settings/SecurityLogin/index.tsx | 51 ++ .../settings/SettingsHeader/index.test.tsx | 78 ++ .../settings/SettingsHeader/index.tsx | 28 +- src/components/sidebar/SafeList/index.tsx | 10 +- src/components/sidebar/Sidebar/index.tsx | 2 +- .../sidebar/SidebarNavigation/config.tsx | 8 + src/components/tx-flow/index.tsx | 1 + src/components/welcome/NewSafe.tsx | 193 +---- src/components/welcome/NewSafeSocial.tsx | 86 ++ .../{ => SafeListDrawer}/DataWidget.tsx | 0 .../welcome/SafeListDrawer/index.tsx | 55 ++ .../welcome/SafeListDrawer/styles.module.css | 33 + .../welcome/WelcomeLogin/WalletLogin.tsx | 63 ++ .../__tests__/WalletLogin.test.tsx | 96 ++ src/components/welcome/WelcomeLogin/index.tsx | 60 ++ .../welcome/WelcomeLogin/styles.module.css | 19 + src/components/welcome/styles.module.css | 147 ++-- src/config/routes.ts | 6 +- src/hooks/__tests__/useTxTracking.test.ts | 39 +- src/hooks/useMnemonicName/dict.ts | 11 +- src/hooks/useMnemonicName/index.ts | 12 +- .../useMnemonicName/useMnemonicName.test.ts | 16 +- .../wallets/__tests__/useOnboard.test.ts | 254 ++++++ src/hooks/wallets/consts.ts | 3 + .../wallets/mpc/__tests__/useMPC.test.ts | 226 +++++ src/hooks/wallets/mpc/useMPC.ts | 92 ++ src/hooks/wallets/mpc/useSocialWallet.ts | 63 ++ src/hooks/wallets/useOnboard.test.ts | 69 -- src/hooks/wallets/useOnboard.ts | 16 +- src/hooks/wallets/wallets.ts | 8 + src/pages/_app.tsx | 5 + src/pages/index.tsx | 4 +- src/pages/settings/security-login.tsx | 23 + src/pages/wc.tsx | 2 +- src/pages/{welcome.tsx => welcome/index.tsx} | 0 src/pages/welcome/social-login.tsx | 17 + src/service-workers/index.ts | 3 + src/service-workers/mpc-core-kit-sw.ts | 325 +++++++ .../analytics/events/createLoadSafe.ts | 11 +- src/services/analytics/events/mpcWallet.ts | 56 ++ src/services/exceptions/ErrorCodes.ts | 3 + src/services/mpc/PasswordRecoveryModal.tsx | 42 + src/services/mpc/SocialLoginModule.ts | 136 +++ src/services/mpc/SocialWalletService.ts | 163 ++++ .../mpc/__mocks__/SocialWalletService.ts | 74 ++ .../mpc/__tests__/SocialWalletService.test.ts | 271 ++++++ src/services/mpc/__tests__/module.test.ts | 125 +++ src/services/mpc/config.ts | 40 + src/services/mpc/icon.ts | 12 + src/services/mpc/interfaces.ts | 52 ++ .../mpc/recovery/DeviceShareRecovery.ts | 59 ++ .../mpc/recovery/SecurityQuestionRecovery.ts | 48 + src/store/__tests__/addressBookSlice.test.ts | 12 + src/store/addressBookSlice.ts | 3 + src/styles/globals.css | 4 + src/tests/builders/eip1193Provider.ts | 10 + src/tests/builders/wallet.ts | 16 + src/utils/addresses.ts | 6 +- src/utils/chains.ts | 1 + yarn.lock | 820 +++++++++++++++++- 129 files changed, 6109 insertions(+), 640 deletions(-) create mode 100644 public/images/common/bar-chart.svg create mode 100644 public/images/common/check-filled.svg create mode 100644 public/images/common/lock-small.svg create mode 100644 public/images/common/lock-warning.svg create mode 100644 public/images/common/lock.svg create mode 100644 public/images/common/shield-off.svg create mode 100644 public/images/common/shield.svg create mode 100644 public/images/logo-text.svg create mode 100644 public/images/welcome/logo-google.svg create mode 100644 src/components/common/ConnectWallet/ConnectionCenter.tsx create mode 100644 src/components/common/ConnectWallet/WalletDetails.tsx create mode 100644 src/components/common/ConnectWallet/__tests__/AccountCenter.test.tsx create mode 100644 src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx create mode 100644 src/components/common/SocialLoginInfo/index.tsx create mode 100644 src/components/common/SocialLoginInfo/styles.module.css create mode 100644 src/components/common/SocialSigner/PasswordRecovery.tsx create mode 100644 src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx create mode 100644 src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx create mode 100644 src/components/common/SocialSigner/index.tsx create mode 100644 src/components/common/SocialSigner/styles.module.css create mode 100644 src/components/common/WalletInfo/index.test.tsx create mode 100644 src/components/common/WalletOverview/index.tsx create mode 100644 src/components/common/WalletOverview/styles.module.css delete mode 100644 src/components/new-safe/create/steps/ConnectWalletStep/index.tsx create mode 100644 src/components/new-safe/create/steps/ReviewStep/index.test.tsx create mode 100644 src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx create mode 100644 src/components/settings/SecurityLogin/SocialSignerExport/index.tsx create mode 100644 src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css create mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx create mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx create mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx create mode 100644 src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css create mode 100644 src/components/settings/SecurityLogin/index.tsx create mode 100644 src/components/settings/SettingsHeader/index.test.tsx create mode 100644 src/components/welcome/NewSafeSocial.tsx rename src/components/welcome/{ => SafeListDrawer}/DataWidget.tsx (100%) create mode 100644 src/components/welcome/SafeListDrawer/index.tsx create mode 100644 src/components/welcome/SafeListDrawer/styles.module.css create mode 100644 src/components/welcome/WelcomeLogin/WalletLogin.tsx create mode 100644 src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx create mode 100644 src/components/welcome/WelcomeLogin/index.tsx create mode 100644 src/components/welcome/WelcomeLogin/styles.module.css create mode 100644 src/hooks/wallets/__tests__/useOnboard.test.ts create mode 100644 src/hooks/wallets/mpc/__tests__/useMPC.test.ts create mode 100644 src/hooks/wallets/mpc/useMPC.ts create mode 100644 src/hooks/wallets/mpc/useSocialWallet.ts delete mode 100644 src/hooks/wallets/useOnboard.test.ts create mode 100644 src/pages/settings/security-login.tsx rename src/pages/{welcome.tsx => welcome/index.tsx} (100%) create mode 100644 src/pages/welcome/social-login.tsx create mode 100644 src/service-workers/mpc-core-kit-sw.ts create mode 100644 src/services/analytics/events/mpcWallet.ts create mode 100644 src/services/mpc/PasswordRecoveryModal.tsx create mode 100644 src/services/mpc/SocialLoginModule.ts create mode 100644 src/services/mpc/SocialWalletService.ts create mode 100644 src/services/mpc/__mocks__/SocialWalletService.ts create mode 100644 src/services/mpc/__tests__/SocialWalletService.test.ts create mode 100644 src/services/mpc/__tests__/module.test.ts create mode 100644 src/services/mpc/config.ts create mode 100644 src/services/mpc/icon.ts create mode 100644 src/services/mpc/interfaces.ts create mode 100644 src/services/mpc/recovery/DeviceShareRecovery.ts create mode 100644 src/services/mpc/recovery/SecurityQuestionRecovery.ts create mode 100644 src/tests/builders/eip1193Provider.ts create mode 100644 src/tests/builders/wallet.ts diff --git a/.env.example b/.env.example index 6642c2c52c..4ee43536d7 100644 --- a/.env.example +++ b/.env.example @@ -40,4 +40,9 @@ NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING= NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING= # Redefine -NEXT_PUBLIC_REDEFINE_API= \ No newline at end of file +NEXT_PUBLIC_REDEFINE_API= + +# Social Login +NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING= +NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION= + diff --git a/.github/workflows/build/action.yml b/.github/workflows/build/action.yml index fd57aada4f..8a8c32e0c1 100644 --- a/.github/workflows/build/action.yml +++ b/.github/workflows/build/action.yml @@ -43,6 +43,8 @@ runs: NEXT_PUBLIC_SAFE_RELAY_SERVICE_URL_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SAFE_GELATO_RELAY_SERVICE_URL_STAGING }} NEXT_PUBLIC_IS_OFFICIAL_HOST: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_IS_OFFICIAL_HOST }} NEXT_PUBLIC_REDEFINE_API: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_REDEFINE_API }} + NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING }} + NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION }} NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_PRODUCTION }} NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING }} NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION: ${{ fromJSON(inputs.secrets).NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION }} diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index 706f7e4da1..ba4f9f8599 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -1,5 +1,7 @@ import * as constants from '../../support/constants' +const welcomeLoginScreen = '[data-testid="welcome-login"]' +const expandMoreIcon = 'svg[data-testid="ExpandMoreIcon"]' const nameInput = 'input[name="name"]' const selectNetworkBtn = '[data-cy="create-safe-select-network"]' const ownerInput = 'input[name^="owners"][name$="name"]' @@ -7,16 +9,17 @@ const ownerAddress = 'input[name^="owners"][name$="address"]' const thresholdInput = 'input[name="threshold"]' export const removeOwnerBtn = 'button[aria-label="Remove owner"]' const connectingContainer = 'div[class*="connecting-container"]' -const createNewSafeBtn = 'span[data-track="create-safe: Open stepper"]' +const createNewSafeBtn = 'span[data-track="create-safe: Continue to creation"]' +const connectWalletBtn = 'Connect wallet' const changeNetworkWarningStr = 'Change your wallet network' const safeAccountSetupStr = 'Safe Account setup' -const policy1_1 = '1/1 policy' +const policy1_2 = '1/1 policy' export const walletName = 'test1-sepolia-safe' -export const defaltSepoliaPlaceholder = 'sepolia-safe' +export const defaltSepoliaPlaceholder = 'Sepolia Safe' export function verifyPolicy1_1() { - cy.contains(policy1_1).should('exist') + cy.contains(policy1_2).should('exist') // TOD: Need data-cy for containers } @@ -49,13 +52,23 @@ export function clickOnCreateNewSafeBtn() { cy.get(createNewSafeBtn).click().wait(1000) } +export function clickOnConnectWalletBtn() { + cy.get(welcomeLoginScreen).within(() => { + cy.get('button').contains(connectWalletBtn).should('be.visible').should('be.enabled').click().wait(1000) + }) +} + export function typeWalletName(name) { cy.get(nameInput).type(name).should('have.value', name) } +export function clearWalletName() { + cy.get(nameInput).clear() +} + export function selectNetwork(network, regex = false) { cy.wait(1000) - cy.get(selectNetworkBtn).should('be.visible').click() + cy.get(expandMoreIcon).eq(1).parents('div').eq(1).click() cy.wait(1000) cy.get('li').contains(network).click() cy.get('body').click() @@ -91,7 +104,7 @@ export function typeOwnerAddress(address, index, clearOnly = false) { } export function clickOnAddNewOwnerBtn() { - cy.contains('button', 'Add new owner').click() + cy.contains('button', 'Add new owner').click().wait(700) } export function addNewOwner(name, address, index) { diff --git a/cypress/e2e/pages/import_export.pages.js b/cypress/e2e/pages/import_export.pages.js index e5b49348ba..d2bf9b050b 100644 --- a/cypress/e2e/pages/import_export.pages.js +++ b/cypress/e2e/pages/import_export.pages.js @@ -25,7 +25,7 @@ export function clickOnImportBtn() { } export function clickOnImportBtnDataImportModal() { - cy.contains(dataImportModalStr).parent().contains('button', 'Import').click() + cy.contains('button', 'Import').click() } export function uploadFile(filePath) { @@ -44,6 +44,10 @@ export function clickOnImportedSafe(safe) { cy.contains(safe).click() } +export function clickOnOpenSafeListSidebar() { + cy.contains('My Safe Accounts').click() +} + export function clickOnClosePushNotificationsBanner() { cy.waitForSelector(() => { return cy.get('h6').contains(enablePushNotificationsStr).siblings('.MuiButtonBase-root').click({ force: true }) diff --git a/cypress/e2e/pages/load_safe.pages.js b/cypress/e2e/pages/load_safe.pages.js index bc0fa83daa..488b343bc6 100644 --- a/cypress/e2e/pages/load_safe.pages.js +++ b/cypress/e2e/pages/load_safe.pages.js @@ -1,6 +1,6 @@ import * as constants from '../../support/constants' -const addExistingAccountBtnStr = 'Add existing Account' +const addExistingAccountBtnStr = 'Add existing one' const contactStr = 'Name, address & network' const invalidAddressFormatErrorMsg = 'Invalid address format' @@ -16,7 +16,7 @@ const ownersConfirmationsStr = 'Owners and confirmations' const transactionStr = 'Transactions' export function openLoadSafeForm() { - cy.contains('button', addExistingAccountBtnStr).click() + cy.contains('a', addExistingAccountBtnStr).click() cy.contains(contactStr) } diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 51d0c658f6..6e2e30aef3 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -19,6 +19,7 @@ const thresholdDropdown = 'div[aria-haspopup="listbox"]' const thresholdOption = 'li[role="option"]' const existingOwnerAddressInput = (index) => `input[name="owners.${index}.address"]` const existingOwnerNameInput = (index) => `input[name="owners.${index}.name"]` +const singleOwnerNameInput = 'input[name="name"]' const disconnectBtnStr = 'Disconnect' const notConnectedStatus = 'Connect' @@ -57,9 +58,9 @@ export function verifyExistingOwnerName(index, name) { cy.get(existingOwnerNameInput(index)).should('have.value', name) } -export function typeExistingOwnerName(index, name) { - cy.get(existingOwnerNameInput(index)).clear().type(name) - main.verifyInputValue(existingOwnerNameInput(index), name) +export function typeExistingOwnerName(name) { + cy.get(singleOwnerNameInput).clear().type(name) + main.verifyInputValue(singleOwnerNameInput, name) } export function verifyOwnerDeletionWindowDisplayed() { diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 9dc90f4723..7f5a8690c3 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -1,29 +1,27 @@ import * as constants from '../../support/constants' import * as main from '../../e2e/pages/main.page' import * as createwallet from '../pages/create_wallet.pages' - import * as owner from '../pages/owners.pages' describe('Safe creation tests', () => { beforeEach(() => { - cy.visit(constants.createNewSafeSepoliaUrl) + cy.visit(constants.welcomeUrl + '?chain=sep') cy.clearLocalStorage() main.acceptCookies() }) it('Verify a Wallet can be connected [C56101]', () => { - owner.waitForConnectionStatus() - cy.visit(constants.welcomeUrl) + createwallet.clickOnCreateNewSafeBtn() owner.clickOnWalletExpandMoreIcon() owner.clickOnDisconnectBtn() - createwallet.clickOnCreateNewSafeBtn() - owner.clickOnConnectBtn() + createwallet.clickOnConnectWalletBtn() createwallet.connectWallet() }) it('Verify Next button is disabled until switching to network is done [C56102]', () => { owner.waitForConnectionStatus() createwallet.selectNetwork(constants.networks.ethereum) + createwallet.clickOnCreateNewSafeBtn() createwallet.checkNetworkChangeWarningMsg() createwallet.verifyNextBtnIsDisabled() createwallet.selectNetwork(constants.networks.sepolia) @@ -32,43 +30,49 @@ describe('Safe creation tests', () => { it('Verify that a new Wallet has default name related to the selected network [C56099]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) }) it('Verify error message is displayed if wallet name input exceeds 50 characters [C56098]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) + createwallet.clearWalletName() }) it('Verify there is no error message is displayed if wallet name input contains less than 50 characters [C56100]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(50)) owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) }) it('Verify current connected account is shown as default owner [C56091]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() owner.clickOnNextBtn() owner.verifyExistingOwnerAddress(0, constants.DEFAULT_OWNER_ADDRESS) }) it('Verify error message is displayed if owner name input exceeds 50 characters [C56092]', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() - owner.typeExistingOwnerName(0, main.generateRandomString(51)) + createwallet.clickOnCreateNewSafeBtn() + owner.typeExistingOwnerName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) }) it('Verify there is no error message is displayed if owner name input contains less than 50 characters [C56093]', () => { owner.waitForConnectionStatus() - owner.clickOnNextBtn() - owner.typeExistingOwnerName(0, main.generateRandomString(50)) + createwallet.clickOnCreateNewSafeBtn() + owner.typeExistingOwnerName(main.generateRandomString(50)) owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) }) it('Verify Add and Remove Owner Row works as expected [C56094]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() owner.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(2) @@ -82,26 +86,34 @@ describe('Safe creation tests', () => { it('Verify Threshold Setup [C56096]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() owner.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() - owner.verifyNumberOfOwners(2) createwallet.clickOnAddNewOwnerBtn() owner.verifyNumberOfOwners(3) - owner.verifyThresholdLimit(1, 3) + createwallet.clickOnAddNewOwnerBtn() + owner.verifyNumberOfOwners(4) + owner.verifyThresholdLimit(1, 4) createwallet.updateThreshold(3) createwallet.removeOwner(1) + owner.verifyThresholdLimit(1, 3) + createwallet.removeOwner(1) owner.verifyThresholdLimit(1, 2) + createwallet.updateThreshold(1) }) it('Verify data persistence [C56103]', () => { const ownerName = 'David' owner.waitForConnectionStatus() - createwallet.typeWalletName(createwallet.walletName) + createwallet.clickOnCreateNewSafeBtn() owner.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() createwallet.typeOwnerName(ownerName, 1) createwallet.typeOwnerAddress(constants.SEPOLIA_OWNER_2, 1) - owner.verifyThresholdLimit(1, 2) + owner.clickOnBackBtn() + createwallet.clearWalletName() + createwallet.typeWalletName(createwallet.walletName) + owner.clickOnNextBtn() owner.clickOnNextBtn() createwallet.verifySafeNameInSummaryStep(createwallet.walletName) createwallet.verifyOwnerNameInSummaryStep(ownerName) @@ -125,12 +137,14 @@ describe('Safe creation tests', () => { it('Verify tip is displayed on right side for threshold 1/1 [C56097]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() owner.clickOnNextBtn() createwallet.verifyPolicy1_1() }) it('Verify address input validation rules [C56095]', () => { owner.waitForConnectionStatus() + createwallet.clickOnCreateNewSafeBtn() owner.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() createwallet.typeOwnerAddress(main.generateRandomString(10), 1) diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/smoke/import_export_data.cy.js index 140b5d9dc1..2bcc125943 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/smoke/import_export_data.cy.js @@ -6,19 +6,19 @@ import * as constants from '../../support/constants' describe('Import Export Data tests', () => { before(() => { cy.clearLocalStorage() - cy.visit(constants.welcomeUrl) + cy.visit(constants.dataSettingsUrl) main.acceptCookies() - file.verifyImportBtnIsVisible() }) it('Verify Safe can be accessed after test file upload [C56111]', () => { const filePath = '../fixtures/data_import.json' const safe = constants.SEPOLIA_CSV_ENTRY.name - file.clickOnImportBtn() file.uploadFile(filePath) file.verifyImportModalData() file.clickOnImportBtnDataImportModal() + cy.visit(constants.welcomeUrl) + file.clickOnOpenSafeListSidebar() file.clickOnImportedSafe(safe) file.clickOnClosePushNotificationsBanner() }) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 6daeacfc92..e9dce33964 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -54,6 +54,7 @@ export const requestPermissionsUrl = '/request-permissions' export const getPermissionsUrl = '/get-permissions' export const appSettingsUrl = '/settings/safe-apps' export const setupUrl = '/settings/setup?safe=' +export const dataSettingsUrl = '/settings/data' export const invalidAppUrl = 'https://my-invalid-custom-app.com/manifest.json' export const validAppUrlJson = 'https://my-valid-custom-app.com/manifest.json' export const validAppUrl = 'https://my-valid-custom-app.com' diff --git a/jest.config.cjs b/jest.config.cjs index 2ba19a6f88..1b06adda63 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -8,13 +8,13 @@ const createJestConfig = nextJest({ // Add any custom config to be passed to Jest const customJestConfig = { setupFilesAfterEnv: ['/jest.setup.js'], + moduleNameMapper: { // Handle module aliases (this will be automatically configured for you soon) '^@/(.*)$': '/src/$1', '^.+\\.(svg)$': '/mocks/svg.js', isows: '/node_modules/isows/_cjs/index.js', }, - transformIgnorePatterns: ['node_modules/(?!isows/)'], testEnvironment: 'jest-environment-jsdom', testEnvironmentOptions: { url: 'http://localhost/balances?safe=rin:0xb3b83bf204C458B461de9B0CD2739DB152b4fa5A' }, globals: { @@ -25,5 +25,5 @@ const customJestConfig = { // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async module.exports = async () => ({ ...(await createJestConfig(customJestConfig)()), - transformIgnorePatterns: ['node_modules/(?!(uint8arrays|multiformats)/)'], + transformIgnorePatterns: ['node_modules/(?!(uint8arrays|multiformats|@web3-onboard/common)/)'], }) diff --git a/next.config.mjs b/next.config.mjs index 1470c8d524..07d1f2a35d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -32,7 +32,7 @@ const nextConfig = { dirs: ['src'], }, experimental: { - optimizePackageImports: ['@mui/material', '@mui/icons-material', 'lodash', 'date-fns'] + optimizePackageImports: ['@mui/material', '@mui/icons-material', 'lodash', 'date-fns'], }, webpack(config) { config.module.rules.push({ diff --git a/package.json b/package.json index 9882a430e1..26ac24be3c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", "type": "module", - "version": "1.21.0", + "version": "1.22.0", "scripts": { "dev": "next dev", "start": "next dev", @@ -56,6 +56,7 @@ "@safe-global/safe-react-components": "^2.0.6", "@sentry/react": "^7.74.0", "@sentry/tracing": "^7.74.0", + "@tkey-mpc/common-types": "^8.2.2", "@truffle/hdwallet-provider": "^2.1.4", "@walletconnect/utils": "^2.10.2", "@walletconnect/web3wallet": "^1.9.2", @@ -66,7 +67,9 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.4.7", + "@web3auth/mpc-core-kit": "^1.1.3", "blo": "^1.1.1", + "bn.js": "^5.2.1", "classnames": "^2.3.1", "date-fns": "^2.29.2", "ethers": "5.7.2", diff --git a/public/images/common/bar-chart.svg b/public/images/common/bar-chart.svg new file mode 100644 index 0000000000..f6beb44899 --- /dev/null +++ b/public/images/common/bar-chart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/common/check-filled.svg b/public/images/common/check-filled.svg new file mode 100644 index 0000000000..284624fc19 --- /dev/null +++ b/public/images/common/check-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/common/lock-small.svg b/public/images/common/lock-small.svg new file mode 100644 index 0000000000..2620c043c8 --- /dev/null +++ b/public/images/common/lock-small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/common/lock-warning.svg b/public/images/common/lock-warning.svg new file mode 100644 index 0000000000..6972e1bb15 --- /dev/null +++ b/public/images/common/lock-warning.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/common/lock.svg b/public/images/common/lock.svg new file mode 100644 index 0000000000..9535a642e0 --- /dev/null +++ b/public/images/common/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/common/shield-off.svg b/public/images/common/shield-off.svg new file mode 100644 index 0000000000..f36482d5ca --- /dev/null +++ b/public/images/common/shield-off.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/common/shield.svg b/public/images/common/shield.svg new file mode 100644 index 0000000000..f7d12a900f --- /dev/null +++ b/public/images/common/shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/logo-text.svg b/public/images/logo-text.svg new file mode 100644 index 0000000000..b71ae734a4 --- /dev/null +++ b/public/images/logo-text.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/welcome/logo-google.svg b/public/images/welcome/logo-google.svg new file mode 100644 index 0000000000..65781d4881 --- /dev/null +++ b/public/images/welcome/logo-google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index 161e9e1bb2..c28d607e09 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -1,48 +1,21 @@ import type { MouseEvent } from 'react' import { useState } from 'react' -import { Box, Button, ButtonBase, Paper, Popover, Typography } from '@mui/material' +import { Box, ButtonBase, Paper, Popover } from '@mui/material' import css from '@/components/common/ConnectWallet/styles.module.css' -import EthHashInfo from '@/components/common/EthHashInfo' import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import useOnboard, { switchWallet } from '@/hooks/wallets/useOnboard' -import { useAppSelector } from '@/store' -import { selectChainById } from '@/store/chainsSlice' -import Identicon from '@/components/common/Identicon' -import ChainSwitcher from '../ChainSwitcher' -import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import WalletInfo, { UNKNOWN_CHAIN_NAME } from '../WalletInfo' +import WalletOverview from '../WalletOverview' +import WalletInfo from '@/components/common/WalletInfo' -const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { +export const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const [anchorEl, setAnchorEl] = useState(null) - const onboard = useOnboard() - const chainInfo = useAppSelector((state) => selectChainById(state, wallet.chainId)) - const addressBook = useAddressBook() - const prefix = chainInfo?.shortName - const handleSwitchWallet = () => { - if (onboard) { - handleClose() - switchWallet(onboard) - } - } - - const handleDisconnect = () => { - if (!wallet) return - - onboard?.disconnectWallet({ - label: wallet.label, - }) - - handleClose() - } - - const handleClick = (event: MouseEvent) => { + const openWalletInfo = (event: MouseEvent) => { setAnchorEl(event.currentTarget) } - const handleClose = () => { + const closeWalletInfo = () => { setAnchorEl(null) } @@ -51,9 +24,15 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { return ( <> - + - + {open ? : } @@ -65,7 +44,7 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { id={id} open={open} anchorEl={anchorEl} - onClose={handleClose} + onClose={closeWalletInfo} anchorOrigin={{ vertical: 'bottom', horizontal: 'center', @@ -81,43 +60,7 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { }} > - - - - {addressBook[wallet.address] || wallet.ens} - - - - - - - - - Wallet - {wallet.label} - - - Connected network - {chainInfo?.chainName || UNKNOWN_CHAIN_NAME} - - - - - - - - + diff --git a/src/components/common/ConnectWallet/ConnectionCenter.tsx b/src/components/common/ConnectWallet/ConnectionCenter.tsx new file mode 100644 index 0000000000..1e04b377d2 --- /dev/null +++ b/src/components/common/ConnectWallet/ConnectionCenter.tsx @@ -0,0 +1,79 @@ +import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' +import madProps from '@/utils/mad-props' +import { Popover, ButtonBase, Typography, Paper, Box } from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import ExpandLessIcon from '@mui/icons-material/ExpandLess' +import classnames from 'classnames' +import { useState, type MouseEvent, type ReactElement } from 'react' + +import KeyholeIcon from '@/components/common/icons/KeyholeIcon' +import WalletDetails from '@/components/common/ConnectWallet/WalletDetails' + +import css from '@/components/common/ConnectWallet/styles.module.css' + +export const ConnectionCenter = ({ isSocialLoginEnabled }: { isSocialLoginEnabled: boolean }): ReactElement => { + const [anchorEl, setAnchorEl] = useState(null) + const open = !!anchorEl + + const handleClick = (event: MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const ExpandIcon = open ? ExpandLessIcon : ExpandMoreIcon + + if (!isSocialLoginEnabled) { + return ( + + + + ) + } + + return ( + <> + + + + + Not connected + palette.error.main }}> + Connect wallet + + + + + + + + + + + + + ) +} + +const useIsSocialLoginEnabled = () => useHasFeature(FEATURES.SOCIAL_LOGIN) + +export default madProps(ConnectionCenter, { + isSocialLoginEnabled: useIsSocialLoginEnabled, +}) diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx new file mode 100644 index 0000000000..5f0442f50a --- /dev/null +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -0,0 +1,30 @@ +import { Box, Divider, SvgIcon, Typography } from '@mui/material' +import type { ReactElement } from 'react' + +import LockIcon from '@/public/images/common/lock.svg' +import SocialSigner from '@/components/common/SocialSigner' +import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' + +const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { + return ( + <> + + + + + + + + + + + or + + + + + + ) +} + +export default WalletDetails diff --git a/src/components/common/ConnectWallet/__tests__/AccountCenter.test.tsx b/src/components/common/ConnectWallet/__tests__/AccountCenter.test.tsx new file mode 100644 index 0000000000..382b7acb1d --- /dev/null +++ b/src/components/common/ConnectWallet/__tests__/AccountCenter.test.tsx @@ -0,0 +1,35 @@ +import { render } from '@/tests/test-utils' +import { AccountCenter } from '@/components/common/ConnectWallet/AccountCenter' +import { type EIP1193Provider } from '@web3-onboard/core' +import { act, waitFor } from '@testing-library/react' + +const mockWallet = { + address: '0x1234567890123456789012345678901234567890', + chainId: '5', + label: '', + provider: null as unknown as EIP1193Provider, +} + +describe('AccountCenter', () => { + it('should open and close the account center on click', async () => { + const { getByText, getByTestId } = render() + + const openButton = getByTestId('open-account-center') + + act(() => { + openButton.click() + }) + + const disconnectButton = getByText('Disconnect') + + expect(disconnectButton).toBeInTheDocument() + + act(() => { + disconnectButton.click() + }) + + await waitFor(() => { + expect(disconnectButton).not.toBeInTheDocument() + }) + }) +}) diff --git a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx new file mode 100644 index 0000000000..5ca16f4512 --- /dev/null +++ b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx @@ -0,0 +1,18 @@ +import { ConnectionCenter } from '@/components/common/ConnectWallet/ConnectionCenter' +import { render } from '@/tests/test-utils' + +describe('ConnectionCenter', () => { + it('displays a Connect wallet button if the social login feature is enabled', () => { + const { getByText, queryByText } = render() + + expect(getByText('Connect wallet')).toBeInTheDocument() + expect(queryByText('Connect')).not.toBeInTheDocument() + }) + + it('displays the ConnectWalletButton if the social login feature is disabled', () => { + const { getByText, queryByText } = render() + + expect(queryByText('Connect wallet')).not.toBeInTheDocument() + expect(getByText('Connect')).toBeInTheDocument() + }) +}) diff --git a/src/components/common/ConnectWallet/index.tsx b/src/components/common/ConnectWallet/index.tsx index aaf6812b1a..45a9f33a9f 100644 --- a/src/components/common/ConnectWallet/index.tsx +++ b/src/components/common/ConnectWallet/index.tsx @@ -1,13 +1,12 @@ import type { ReactElement } from 'react' import useWallet from '@/hooks/wallets/useWallet' import AccountCenter from '@/components/common/ConnectWallet/AccountCenter' -import ConnectWalletButton from './ConnectWalletButton' -import css from './styles.module.css' +import ConnectionCenter from './ConnectionCenter' const ConnectWallet = (): ReactElement => { const wallet = useWallet() - return
{wallet ? : }
+ return wallet ? : } export default ConnectWallet diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index 24057834f2..b41bbc8399 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -1,5 +1,6 @@ -.container { - padding: 0 var(--space-2); +.connectedContainer { + display: flex; + align-items: center; } .buttonContainer { @@ -7,18 +8,23 @@ align-items: center; text-align: left; gap: var(--space-1); + padding: 0 var(--space-2); } .popoverContainer { padding: var(--space-2); - width: 250px; + width: 300px; display: flex; flex-direction: column; align-items: center; - gap: var(--space-2); + gap: var(--space-1); border: 1px solid var(--color-border-light); } +.largeGap { + gap: var(--space-2); +} + .addressName { text-align: center; overflow: hidden; @@ -27,6 +33,18 @@ width: 100%; } +.profileImg { + border-radius: var(--space-2); + width: 32px; + height: 32px; +} + +.profileData { + display: flex; + flex-direction: column; + align-items: flex-start; +} + .rowContainer { align-self: stretch; margin-left: calc(var(--space-2) * -1); @@ -45,3 +63,29 @@ .row:last-of-type { border-bottom: 1px solid var(--color-border-light); } + +.pairingDetails { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); +} + +.loginButton { + min-height: 42px; +} + +.loginError { + width: 100%; + margin: 0; +} + +@media (max-width: 599.95px) { + .socialLoginInfo > div > div { + display: none; + } + + .notConnected { + display: none; + } +} diff --git a/src/components/common/ConnectWallet/useConnectWallet.ts b/src/components/common/ConnectWallet/useConnectWallet.ts index 9dc10e7930..b3f832229e 100644 --- a/src/components/common/ConnectWallet/useConnectWallet.ts +++ b/src/components/common/ConnectWallet/useConnectWallet.ts @@ -1,19 +1,17 @@ -import { useMemo } from 'react' +import { useCallback } from 'react' import useOnboard, { connectWallet } from '@/hooks/wallets/useOnboard' import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' -const useConnectWallet = (): (() => void) => { +const useConnectWallet = () => { const onboard = useOnboard() - return useMemo(() => { + return useCallback(() => { if (!onboard) { - return () => {} + return Promise.resolve(undefined) } - return () => { - trackEvent(OVERVIEW_EVENTS.OPEN_ONBOARD) - connectWallet(onboard) - } + trackEvent(OVERVIEW_EVENTS.OPEN_ONBOARD) + return connectWallet(onboard) }, [onboard]) } diff --git a/src/components/common/ErrorBoundary/index.tsx b/src/components/common/ErrorBoundary/index.tsx index e277db308a..f25c1bbcfa 100644 --- a/src/components/common/ErrorBoundary/index.tsx +++ b/src/components/common/ErrorBoundary/index.tsx @@ -33,7 +33,7 @@ const ErrorBoundary: FallbackRender = ({ error, componentStack }) => { {componentStack} )} - + Go home diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx index 1fbbd76d89..34041dbdfb 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx @@ -71,7 +71,7 @@ const SrcEthHashInfo = ({ {name && ( - + {name} )} diff --git a/src/components/common/Footer/index.tsx b/src/components/common/Footer/index.tsx index afd19922e0..545b1f43aa 100644 --- a/src/components/common/Footer/index.tsx +++ b/src/components/common/Footer/index.tsx @@ -11,7 +11,7 @@ import MUILink from '@mui/material/Link' import { IS_DEV, IS_OFFICIAL_HOST } from '@/config/constants' const footerPages = [ - AppRoutes.welcome, + AppRoutes.welcome.index, AppRoutes.settings.index, AppRoutes.imprint, AppRoutes.privacy, diff --git a/src/components/common/Header/index.tsx b/src/components/common/Header/index.tsx index c18ebcd039..6919ca1526 100644 --- a/src/components/common/Header/index.tsx +++ b/src/components/common/Header/index.tsx @@ -34,7 +34,7 @@ const Header = ({ onMenuToggle, onBatchToggle }: HeaderProps): ReactElement => { const enableWc = !!chain && hasFeature(chain, FEATURES.NATIVE_WALLETCONNECT) // Logo link: if on Dashboard, link to Welcome, otherwise to the root (which redirects to either Dashboard or Welcome) - const logoHref = router.pathname === AppRoutes.home ? AppRoutes.welcome : AppRoutes.index + const logoHref = router.pathname === AppRoutes.home ? AppRoutes.welcome.index : AppRoutes.index const handleMenuToggle = () => { if (onMenuToggle) { diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index efe261b122..5e3ac1e676 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -1,20 +1,34 @@ import Link from 'next/link' import type { SelectChangeEvent } from '@mui/material' -import { MenuItem, Select, Skeleton } from '@mui/material' +import { MenuItem, Select, Skeleton, Tooltip } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import useChains from '@/hooks/useChains' import { useRouter } from 'next/router' import ChainIndicator from '../ChainIndicator' import css from './styles.module.css' import { useChainId } from '@/hooks/useChainId' -import type { ReactElement } from 'react' +import { type ReactElement, forwardRef } from 'react' import { useCallback } from 'react' import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' +import useWallet from '@/hooks/wallets/useWallet' +import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' +import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -const keepPathRoutes = [AppRoutes.welcome, AppRoutes.newSafe.create, AppRoutes.newSafe.load] +const keepPathRoutes = [AppRoutes.welcome.index, AppRoutes.newSafe.create, AppRoutes.newSafe.load] + +const MenuWithTooltip = forwardRef(function MenuWithTooltip(props: any, ref) { + return ( + +
    + {props.children} +
+
+ ) +}) const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => { + const wallet = useWallet() const { configs } = useChains() const chainId = useChainId() const router = useRouter() @@ -53,6 +67,8 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => } } + const isSocialLogin = isSocialLoginWallet(wallet?.label) + return configs.length ? (