diff --git a/.env.example b/.env.example index d65c58a6..f57b1e3c 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ REACT_APP_WALLET_VERSION=1.0.10 REACT_APP_ENV=dev REACT_APP_GA4_MEASUREMENT_ID= REACT_APP_SENTRY_DSN= +REACT_APP_WC_PROJECT_ID= ##################### ##### WebWallet ##### diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 30d40004..38aee100 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,20 @@ -:clipboard: Add associated issues, tickets, docs URL here. - -closes: +:clipboard: closes: ## Overview Describe what your Pull Request is about in a few sentences. +## Tasks + +- [ ] No debug console statement added +- [ ] Should use only typescript. +- [ ] Does have unit test for each touched file with 90% coverage +- [ ] Does have an integration test if any UI changes. + + ## Changes -Describe your changes and implementation choices. More details make PRs easier to review. +Describe your changes and implementation choices - Change 1 - Change 2 @@ -16,4 +22,12 @@ Describe your changes and implementation choices. More details make PRs easier t ## Testing -Describe how to test your new feature/bug fix and if possible, a step by step guide on how to demo this. +Snapshots for test coverage for changed files. + + +## UI Snaps + +Add screenshots or video if any visuals changes are there so would be helpful for review. + + + diff --git a/.github/workflows/coverage-linting.yml b/.github/workflows/coverage-linting.yml index f6fad010..ec440e1f 100644 --- a/.github/workflows/coverage-linting.yml +++ b/.github/workflows/coverage-linting.yml @@ -21,7 +21,7 @@ jobs: run: git fetch - name: Setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16.x' @@ -47,6 +47,7 @@ jobs: env: FORCE_COLOR: 1 SKIP_PREFLIGHT_CHECK: true + REACT_APP_WALLET_VERSION: 0.9.1 - uses: codecov/codecov-action@v3 with: @@ -62,7 +63,7 @@ jobs: - uses: actions/checkout@v4 - name: Fetch history run: git fetch - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: '16.x' diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 35b42709..00540650 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.16.0 cache: 'yarn' diff --git a/.github/workflows/secret-detect.yml b/.github/workflows/secret-detect.yml index 72535d8f..954e10a7 100644 --- a/.github/workflows/secret-detect.yml +++ b/.github/workflows/secret-detect.yml @@ -14,6 +14,6 @@ jobs: - name: Run Yelp's detect-secrets uses: RobertFischer/detect-secrets-action@v2.0.0 - name: Commit back .secrets.baseline (if it was missing) - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "build(detect-secrets): Commit the newly-generated .secrets.baseline file" diff --git a/.gitignore b/.gitignore index 408f1441..2e9a9c73 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ coverage *.log # gateway -/build \ No newline at end of file +/build +.idea/ \ No newline at end of file diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 71123fc6..4801c20f 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,7 +1,7 @@ { "*.{ts,tsx}": [ - "yarn format:check", - "yarn lint:check" + "yarn format:fix", + "yarn lint:fix" ], "!(__tests__|*stories|*test|styles).{ts,tsx}": [ "yarn test --passWithNoTests" diff --git a/__mocks__/@walletconnect/ethereum-provider.ts b/__mocks__/@walletconnect/ethereum-provider.ts new file mode 100644 index 00000000..7d01e0c0 --- /dev/null +++ b/__mocks__/@walletconnect/ethereum-provider.ts @@ -0,0 +1,41 @@ +/* + Since we've installed @walletconnect/ethereum-provider, + it has led to dependency issues during Jest testing. + To resolve this, we need to mock the entire 'node_modules' itself, + as unit testing focuses solely on our code + + sample crash report. + ``` + + TypeError: Cannot read properties of undefined (reading 'base16') + + 15 | limitations under the License. + 16 | + > 17 | import {EthereumProvider} from '@walletconnect/ethereum-provider' + | ^ + + at Object. (node_modules/uint8arrays/cjs/src/util/bases.js:42:21) + at Object. (node_modules/uint8arrays/cjs/src/from-string.js:5:13) + at Object. (node_modules/uint8arrays/cjs/src/index.js:8:18) + at Object. (node_modules/@walletconnect/utils/dist/index.cjs.js:1:324) + at Object. (node_modules/@walletconnect/ethereum-provider/dist/index.cjs.js:1:177) + at Object. (src/services/wallet.service.js:17:1) + at Object. (src/services/networkService.js:86:1) + at Object. (src/components/global/addToMetamask/index.tsx:4:1) + at Object.(src / components / global / index.ts: 1: 1) + + ``` +*/ + +export const EthereumProvider = jest.fn() + +EthereumProvider.mockImplementation(() => { + return { + on: jest.fn(), + connect: jest.fn(), + enable: jest.fn(), + disconnect: jest.fn(), + } +}) + +export default EthereumProvider diff --git a/buildspec.yml b/buildspec.yml new file mode 100644 index 00000000..7d481e0a --- /dev/null +++ b/buildspec.yml @@ -0,0 +1,20 @@ +version: 0.2 + +phases: + install: + runtime-versions: + nodejs: 18 + commands: + - n 16 + - yarn + + build: + commands: + - REACT_APP_WALLET_VERSION=$(git rev-parse --short HEAD) yarn build:prod + +artifacts: + # include all files required to run the application + files: + - '**/*' + discard-paths: no + base-directory: build diff --git a/cypress.config.ts b/cypress.config.ts index 54305ed9..81d6468e 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -22,4 +22,5 @@ export default defineConfig({ env: { target_hash: process.env.CYPRESS_TEST_HASH, }, + includeShadowDom: true, }) diff --git a/cypress/e2e/helpers/base/constants.ts b/cypress/e2e/helpers/base/constants.ts index e9e86f67..de3a3e66 100644 --- a/cypress/e2e/helpers/base/constants.ts +++ b/cypress/e2e/helpers/base/constants.ts @@ -1,4 +1,4 @@ -import { MetamaskNetwork } from './types' +import { MetamaskNetwork, NetworkTestInfo } from './types' export const Binance: MetamaskNetwork = { networkName: 'Binance Mainnet', @@ -35,5 +35,81 @@ export const AvalancheTestnet: MetamaskNetwork = { blockExplorer: 'https://testnet.snowtrace.io/', isTestnet: true, } +export const EthereumInfo: NetworkTestInfo = { + networkName: 'Ethereum', + networkAbbreviation: 'ETHEREUM', + isTestnet: false, +} +export const BinanceInfo: NetworkTestInfo = { + networkName: 'Binance Smart Chain', + networkAbbreviation: 'BNB', + isTestnet: false, +} + +export const AvalancheInfo: NetworkTestInfo = { + networkName: 'Avalanche Mainnet C-Chain', + networkAbbreviation: 'AVAX', + isTestnet: false, +} + +export const MainnetL1Networks: NetworkTestInfo[] = [ + EthereumInfo, + BinanceInfo, + AvalancheInfo, +] + +export const MainnetL2Networks: NetworkTestInfo[] = [ + { + networkName: 'Boba ETH', + networkAbbreviation: 'Boba Eth', + isTestnet: false, + }, + { + networkName: 'Boba BNB', + networkAbbreviation: 'Boba BNB', + isTestnet: false, + }, + { + networkName: 'Boba Avalanche', + networkAbbreviation: 'Boba Avalanche', + isTestnet: false, + }, +] + +export const EthereumGoerliInfo: NetworkTestInfo = { + networkName: 'Ethereum (Goerli)', + networkAbbreviation: 'ETHEREUM', + isTestnet: true, +} + +export const TestnetL1Networks: NetworkTestInfo[] = [ + EthereumGoerliInfo, + { + networkName: 'BNB Testnet', + networkAbbreviation: 'BNB', + isTestnet: true, + }, + { + networkName: 'Fuji Testnet', + networkAbbreviation: 'AVAX', + isTestnet: true, + }, +] -// Update the configs for boba networks TESTNET / MAINNET +export const TestnetL2Networks: NetworkTestInfo[] = [ + { + networkName: 'Boba (Goerli)', + networkAbbreviation: 'Boba (Goerli)', + isTestnet: true, + }, + { + networkName: 'Boba BNB Testnet', + networkAbbreviation: 'Boba BNB', + isTestnet: true, + }, + { + networkName: 'Boba Fuji Testnet', + networkAbbreviation: 'Boba Fuji', + isTestnet: true, + }, +] diff --git a/cypress/e2e/helpers/base/page.footer.ts b/cypress/e2e/helpers/base/page.footer.ts index d6f80253..989074e3 100644 --- a/cypress/e2e/helpers/base/page.footer.ts +++ b/cypress/e2e/helpers/base/page.footer.ts @@ -17,4 +17,8 @@ export default class PageFooter { // @ts-ignore return cy.get('#socialLinks').contains(/^v\w+/) } + + gasDetailsInfo() { + return cy.get('#gasDetails').find('div') + } } diff --git a/cypress/e2e/helpers/base/page.ts b/cypress/e2e/helpers/base/page.ts index 5e951124..c3da66ad 100644 --- a/cypress/e2e/helpers/base/page.ts +++ b/cypress/e2e/helpers/base/page.ts @@ -3,6 +3,7 @@ import Base from './base' import PageHeader from './page.header' import PageFooter from './page.footer' import { ReduxStore } from './store' +import { pageTitleWhiteList } from '../../../../src/components/layout/PageTitle/constants' export default class Page extends Base { header: PageHeader @@ -10,6 +11,7 @@ export default class Page extends Base { store: ReduxStore walletConnectButtonText: string id: string + title: string constructor() { super() this.store = new ReduxStore() @@ -17,27 +19,49 @@ export default class Page extends Base { this.footer = new PageFooter() this.id = 'header' this.walletConnectButtonText = 'Connect Wallet' + this.title = 'Bridge' } visit() { cy.visit(`/${this.id}`) } + + isReady() { + this.store.verifyReduxStoreSetup('baseEnabled', true) + } + withinPage() { return cy.get(`#${this.id}`) } + getTitle() { + return cy.get(`#title`) + } + connectWallet() { this.withinPage() .contains('button', this.walletConnectButtonText) .should('exist') .click() } + requestMetamaskConnect() { this.connectWallet() - cy.get('#connectMetaMask').should('exist').click() + this.getModal().contains('MetaMask').should('exist').click() + } + + requestWCConnect() { + this.connectWallet() + this.getModal().contains('WalletConnect').should('exist').click() + } + + checkWCQROpen() { + cy.wait(1000) + + cy.get('body').find('wcm-modal').should('exist') } - setNetworkTo(network: 'BNB' | 'AVAX' | 'ETH') { + setNetworkTo(network: 'BNB' | 'AVAX' | 'ETH', type = 'Mainnet') { const bnbConfig = { network: 'BNB', name: { @@ -46,7 +70,7 @@ export default class Page extends Base { }, networkIcon: 'bnb', chainIds: { L1: '56', L2: '56288' }, - networkType: 'Mainnet', + networkType: type, } const avaxConfig = { @@ -57,10 +81,26 @@ export default class Page extends Base { }, networkIcon: 'avax', chainIds: { L1: '43114', L2: '43288' }, - networkType: 'Mainnet', + networkType: type, + } + + const ethConfig = { + network: 'ETHEREUM', + name: { + l1: 'Mainnet', + l2: 'Boba L2', + }, + networkIcon: 'ethereum', + chainIds: { L1: '1', L2: '288' }, + networkType: type, } - const payload = network === 'BNB' ? bnbConfig : avaxConfig + let payload = ethConfig + if (network === 'BNB') { + payload = bnbConfig + } else if (network === 'AVAX') { + payload = avaxConfig + } cy.window().its('store').invoke('dispatch', { type: 'NETWORK/SET', @@ -68,13 +108,22 @@ export default class Page extends Base { }) } + accountConnected() { + this.store.verifyReduxStoreSetup('accountEnabled', true) + this.store + .getReduxStore() + .its('setup') + .its('walletAddress') + .should('not.be.empty') + } + checkNaviagtionListBinanace() { this.header .getNavigationLinks() .should('not.be.empty') .and(($p) => { - // should have found 4 elements for Binanace - expect($p).to.have.length(4) + // should have found 3 elements for Binanace + expect($p).to.have.length(3) // // use jquery's map to grab all of their classes // // jquery's map returns a new jquery object @@ -82,19 +131,23 @@ export default class Page extends Base { return Cypress.$(el).attr('href') }) // call classes.get() to make this a plain array - expect(links.get()).to.deep.eq([ - '/bridge', - '/bridge', - '/history', - '/earn', - ]) + expect(links.get()).to.deep.eq(['/bridge', '/bridge', '/history']) // get labels and verify const labels = $p.map((i, el) => { return Cypress.$(el).text() }) - expect(labels.get()).to.deep.eq(['', 'Bridge', 'History', 'Earn']) + expect(labels.get()).to.deep.eq(['', 'Bridge', 'History']) + }) + } + + validateApplicationBanner() { + cy.get('[data-testid="banner-item"]') + .should('not.be.empty') + .should('be.visible') + .and(($p) => { + expect($p).to.have.length(1) }) } @@ -103,8 +156,8 @@ export default class Page extends Base { .getNavigationLinks() .should('not.be.empty') .and(($p) => { - // should have found 4 elements for Avalanche - expect($p).to.have.length(4) + // should have found 3 elements for Avalanche + expect($p).to.have.length(3) // // use jquery's map to grab all of their classes // // jquery's map returns a new jquery object @@ -112,19 +165,14 @@ export default class Page extends Base { return Cypress.$(el).attr('href') }) // call classes.get() to make this a plain array - expect(links.get()).to.deep.eq([ - '/bridge', - '/bridge', - '/history', - '/earn', - ]) + expect(links.get()).to.deep.eq(['/bridge', '/bridge', '/history']) // get labels and verify const labels = $p.map((i, el) => { return Cypress.$(el).text() }) - expect(labels.get()).to.deep.eq(['', 'Bridge', 'History', 'Earn']) + expect(labels.get()).to.deep.eq(['', 'Bridge', 'History']) }) } @@ -133,8 +181,8 @@ export default class Page extends Base { .getNavigationLinks() .should('not.be.empty') .and(($p) => { - // should have found 6 elements for Ethereum - expect($p).to.have.length(6) + // should have found 5 elements for Ethereum + expect($p).to.have.length(5) // // use jquery's map to grab all of their classes // // jquery's map returns a new jquery object @@ -146,9 +194,8 @@ export default class Page extends Base { '/bridge', '/bridge', '/history', - '/earn', '/stake', - '/DAO', + '/dao', ]) // get labels and verify @@ -160,7 +207,6 @@ export default class Page extends Base { '', 'Bridge', 'History', - 'Earn', 'Stake', 'Dao', ]) @@ -201,24 +247,27 @@ export default class Page extends Base { } handleNetworkSwitchModals(networkAbbreviation: string, isTestnet: boolean) { - cy.get( - `button[label="Switch to ${networkAbbreviation} ${ - isTestnet ? 'Testnet' : '' - } network"]`, - { timeout: 90000 } - ) + this.getModal() + .find( + `button[label="Switch to ${networkAbbreviation} ${ + isTestnet ? 'Testnet' : '' + } network"]`, + { timeout: 90000 } + ) .should('exist') .click() this.store.verifyReduxStoreSetup('accountEnabled', false) this.store.verifyReduxStoreSetup('baseEnabled', false) + this.store.verifyReduxStoreSetup('baseEnabled', true) - cy.get( - `button[label="Connect to the ${networkAbbreviation} ${ - isTestnet ? 'Testnet' : '' - } network"]`, - { timeout: 90000 } - ) + this.getModal() + .find( + `button[label="Connect to the ${networkAbbreviation} ${ + isTestnet ? 'Testnet' : 'Mainnet' + } network"]`, + { timeout: 90000 } + ) .should('exist') .click() } @@ -330,4 +379,39 @@ export default class Page extends Base { this.footer.getCompanyInfo().should('be.visible') this.footer.getVersionInfo().should('be.visible') } + checkTitle() { + if (this.id === 'bridge') { + this.withinPage().contains(this.title).should('exist') + } else { + this.getTitle().contains(this.title).should('exist') + } + } + checkDescription() { + const webPage = pageTitleWhiteList.find( + (whiteListedPage) => whiteListedPage.path === '/' + this.id.toLowerCase() + ) + const slogan = webPage ? webPage.slug : '' + if (!slogan) { + return assert(false) + } + this.getTitle().contains(slogan) + } + + checkGasWatcherListingInETH() { + this.footer + .gasDetailsInfo() + .should('not.be.empty') + .and(($p) => { + expect($p).to.have.length(6) + }) + } + + checkGasWatcherListingInBNB() { + this.footer + .gasDetailsInfo() + .should('not.be.empty') + .and(($p) => { + expect($p).to.have.length(5) + }) + } } diff --git a/cypress/e2e/helpers/base/store.ts b/cypress/e2e/helpers/base/store.ts index c4d59089..ee9138a5 100644 --- a/cypress/e2e/helpers/base/store.ts +++ b/cypress/e2e/helpers/base/store.ts @@ -24,4 +24,11 @@ export class ReduxStore { .should('exist') .should('equal', expectedValue) } + + allowBaseEnabledToUpdate(accountConnected: boolean) { + if (!accountConnected) { + this.verifyReduxStoreSetup('baseEnabled', false) + this.verifyReduxStoreSetup('baseEnabled', true) + } + } } diff --git a/cypress/e2e/helpers/base/types.ts b/cypress/e2e/helpers/base/types.ts index d4e4211a..89c1eecb 100644 --- a/cypress/e2e/helpers/base/types.ts +++ b/cypress/e2e/helpers/base/types.ts @@ -6,3 +6,9 @@ export type MetamaskNetwork = { blockExplorer: string isTestnet: boolean } + +export type NetworkTestInfo = { + networkName: string + networkAbbreviation: string + isTestnet: boolean +} diff --git a/cypress/e2e/helpers/bridge.ts b/cypress/e2e/helpers/bridge.ts index d4c2454a..d5ac4cc3 100644 --- a/cypress/e2e/helpers/bridge.ts +++ b/cypress/e2e/helpers/bridge.ts @@ -1,14 +1,15 @@ /// import Page from './base/page' import { Layer } from '../../../src/util/constant' -import { ReduxStore } from './base/store' +import { NetworkTestInfo } from './base/types' +import { EthereumGoerliInfo } from './base/constants' export default class Bridge extends Page { constructor() { super() this.id = 'bridge' - this.store = new ReduxStore() this.walletConnectButtonText = 'Connect Wallet' + this.title = 'Bridge' } switchNetworkType(network: string, isTestnet: boolean, newNetwork: boolean) { @@ -121,4 +122,124 @@ export default class Bridge extends Page { .should('have.length', 1) .click() } + + checkThirdPartyTabInETH() { + cy.get('[data-testid="third-party-btn"]').should('be.visible').click() + + cy.contains('Third party bridges').should('be.visible') + + const bridgelist = cy.get('a[data-testid="bridge-item"]') + bridgelist.should('not.be.empty').and((bridgeItems) => { + // should have 12 elements. + expect(bridgeItems).to.have.length(12) + + const links = bridgeItems.map((i, el) => { + return Cypress.$(el).attr('href') + }) + + expect(links.get()).to.deep.eq([ + 'https://boba.banxa.com/', + 'https://boba.network/project/beamer-bridge/', + 'https://boba.network/project/boringdao/', + 'https://boba.network/project/celer/', + 'https://boba.network/project/chainswap/', + 'https://boba.network/project/connext/', + 'https://boba.network/project/layerswap-io/', + 'https://boba.network/project/multichain/', + 'https://boba.network/project/rango-exchange/', + 'https://boba.network/project/rubic-exchange/', + 'https://boba.network/project/synapse/', + 'https://boba.network/project/via-protocol/', + ]) + + const labels = bridgeItems.map((i, el) => { + return Cypress.$(el).text() + }) + + expect(labels.get()).to.deep.eq([ + 'Banxa', + 'Beamer Bridge', + 'BoringDAO', + 'Celer', + 'Chainswap', + 'Connext', + 'Layerswap', + 'Multichain', + 'Rango Exchange', + 'Rubic Exchange', + 'Synapse', + 'Via Protocol', + ]) + }) + } + + checkThirdPartyTabInBNB() { + cy.get('[data-testid="third-party-btn"]').should('be.visible').click() + cy.contains('Third party bridges').should('be.visible') + cy.contains('No bridges available').should('be.visible') + } + + openNetworkModal(networkName: string) { + this.withinPage().contains(networkName).should('exist').click() + } + selectNetworkFromModal(networkName: string) { + this.getModal().contains(networkName).should('exist').click() + } + + clickThroughNetworksInModals( + l1Networks: NetworkTestInfo[], + l2Networks: NetworkTestInfo[], + accountConnected: boolean + ) { + for (let i = 0; i < 3; i++) { + this.withinPage().contains(l2Networks[i].networkName).should('exist') + + this.openNetworkModal(l1Networks[i].networkName) + const nextNetwork = l1Networks[(i + 1) % 3] + this.selectNetworkFromModal(nextNetwork.networkName) + if (accountConnected) { + this.handleNetworkSwitchModals( + nextNetwork.networkAbbreviation, + nextNetwork.isTestnet + ) + if (nextNetwork.networkName === l1Networks[0].networkName) { + this.allowNetworkSwitch() + } else { + this.allowNetworkToBeAddedAndSwitchedTo() + } + this.checkNetworkSwitchSuccessful(nextNetwork.networkAbbreviation) + } else { + this.store.allowBaseEnabledToUpdate(accountConnected) + } + } + } + switchToTestnet( + networkAbbreviation: string = EthereumGoerliInfo.networkAbbreviation, + newNetwork: boolean = false + ) { + this.withinPage() + .find('[data-testid="setting-btn"]') + .should('exist') + .click() + this.getModal() + .find('[data-testid="switch-label"]') + .should('have.length', 2) + .first() + .click() + + this.store.verifyReduxStoreNetwork('activeNetworkType', 'Testnet') + + this.handleNetworkSwitchModals(networkAbbreviation, true) + if ( + networkAbbreviation === EthereumGoerliInfo.networkAbbreviation || + !newNetwork + ) { + this.allowNetworkSwitch() + } else { + this.allowNetworkToBeAddedAndSwitchedTo() + } + + this.store.verifyReduxStoreSetup('accountEnabled', true) + this.store.verifyReduxStoreSetup('baseEnabled', true) + } } diff --git a/cypress/e2e/helpers/dao.ts b/cypress/e2e/helpers/dao.ts new file mode 100644 index 00000000..db2fa806 --- /dev/null +++ b/cypress/e2e/helpers/dao.ts @@ -0,0 +1,10 @@ +import Page from './base/page' + +export default class Dao extends Page { + constructor() { + super() + this.id = 'DAO' + this.walletConnectButtonText = 'Connect Wallet' + this.title = 'DAO' + } +} diff --git a/cypress/e2e/helpers/history.ts b/cypress/e2e/helpers/history.ts index 7a2c412d..d24d17f2 100644 --- a/cypress/e2e/helpers/history.ts +++ b/cypress/e2e/helpers/history.ts @@ -32,6 +32,7 @@ export default class History extends Page { this.networkLayerFrom = Layer.L1 this.fromNetwork = 'All Networks' this.toNetwork = 'All Networks' + this.title = 'History' } getSearchInput() { return this.withinPage() diff --git a/cypress/e2e/helpers/stake.ts b/cypress/e2e/helpers/stake.ts new file mode 100644 index 00000000..500bb9c8 --- /dev/null +++ b/cypress/e2e/helpers/stake.ts @@ -0,0 +1,10 @@ +import Page from './base/page' + +export default class Stake extends Page { + constructor() { + super() + this.id = 'stake' + this.walletConnectButtonText = 'Connect Wallet' + this.title = 'Stake' + } +} diff --git a/cypress/e2e/specs/bridge.spec.cy.ts b/cypress/e2e/specs/bridge.spec.cy.ts new file mode 100644 index 00000000..cd3e4a04 --- /dev/null +++ b/cypress/e2e/specs/bridge.spec.cy.ts @@ -0,0 +1,31 @@ +import { LAYER } from '../../../src/util/constant' +import Bridge from '../helpers/bridge' +const bridge = new Bridge() + +describe('Testing Entire Site', () => { + describe('Bridge', () => { + describe('Before wallet is connected', () => { + before(() => { + bridge.visit() + bridge.store.verifyReduxStoreSetup('baseEnabled', true) + }) + + describe('Bridge Layout', () => { + it('Should have the correct title', () => { + bridge.checkTitle() + }) + }) + + describe('3rd party bridges', () => { + it('Should have third party bridge tab with correct details', () => { + bridge.setNetworkTo('ETH') + bridge.checkThirdPartyTabInETH() + }) + it('Should have third party bridge tab with correct details in case of BNB', () => { + bridge.setNetworkTo('BNB') + bridge.checkThirdPartyTabInBNB() + }) + }) + }) + }) +}) diff --git a/cypress/e2e/specs/dao.spec.cy.ts b/cypress/e2e/specs/dao.spec.cy.ts new file mode 100644 index 00000000..df8c56ee --- /dev/null +++ b/cypress/e2e/specs/dao.spec.cy.ts @@ -0,0 +1,19 @@ +import Dao from '../helpers/dao' +const dao = new Dao() + +describe('Testing Entire Site', () => { + describe('Dao', () => { + describe('Before wallet is connected', () => { + before(() => { + dao.visit() + dao.store.verifyReduxStoreSetup('baseEnabled', true) + }) + describe('Dao Layout', () => { + it('Should have the correct title', () => { + dao.checkTitle() + dao.checkDescription() + }) + }) + }) + }) +}) diff --git a/cypress/e2e/specs/flow/connect.spec.cy.ts b/cypress/e2e/specs/flow/connect.spec.cy.ts new file mode 100644 index 00000000..e5dafb61 --- /dev/null +++ b/cypress/e2e/specs/flow/connect.spec.cy.ts @@ -0,0 +1,54 @@ +import { + MainnetL1Networks, + MainnetL2Networks, + TestnetL1Networks, + TestnetL2Networks, +} from '../../helpers/base/constants' +import Bridge from '../../helpers/bridge' + +const bridge = new Bridge() + +describe('Connect flow', () => { + before(() => { + bridge.visit() + bridge.store.verifyReduxStoreSetup('baseEnabled', true) + }) + + describe('Metamask', () => { + before(() => { + bridge.changeMetamaskNetwork('ethereum') + }) + after(() => { + bridge.disconnectWallet() + }) + + it('Should Connect to L1', () => { + bridge.requestMetamaskConnect() + bridge.connectMetamask() + }) + it('Should switch through Mainnet networks using Network Picker Modal', () => { + bridge.clickThroughNetworksInModals( + MainnetL1Networks, + MainnetL2Networks, + true + ) + }) + it('Switch to testnet', () => { + bridge.switchToTestnet() + }) + it('Should switch through Testnet networks using Network Modal', () => { + bridge.clickThroughNetworksInModals( + TestnetL1Networks, + TestnetL2Networks, + true + ) + }) + }) + + describe('- WalletConnect', () => { + it('Should open connect wallet QR dialog', () => { + bridge.requestWCConnect() + bridge.checkWCQROpen() + }) + }) +}) diff --git a/cypress/e2e/specs/history.spec.cy.ts b/cypress/e2e/specs/history.spec.cy.ts new file mode 100644 index 00000000..59294034 --- /dev/null +++ b/cypress/e2e/specs/history.spec.cy.ts @@ -0,0 +1,19 @@ +import History from '../helpers/history' +const history = new History() + +describe('Testing Entire Site', () => { + describe('History', () => { + describe('Before wallet is connected', () => { + before(() => { + history.visit() + history.store.verifyReduxStoreSetup('baseEnabled', true) + }) + describe('History Layout', () => { + it('Should have the correct title', () => { + history.checkTitle() + history.checkDescription() + }) + }) + }) + }) +}) diff --git a/cypress/e2e/specs/layout.spec.cy.ts b/cypress/e2e/specs/layout.spec.cy.ts index 71bb45e7..290cb70b 100644 --- a/cypress/e2e/specs/layout.spec.cy.ts +++ b/cypress/e2e/specs/layout.spec.cy.ts @@ -22,6 +22,12 @@ describe('Page Layout', () => { }) }) + describe('Application Banner', () => { + it('Should see the earn deprecation banner and remove on close.', () => { + page.validateApplicationBanner() + }) + }) + describe('Footer', () => { it('Navigation links', () => { page.checkFooterLinks() @@ -29,6 +35,14 @@ describe('Page Layout', () => { it('Social links', () => { page.checkSocialMediaLinks() }) + it('Gas details should be visible', () => { + page.setNetworkTo('ETH') + page.checkGasWatcherListingInETH() + }) + it('Gas details should be visible with no status verifier value in cas of BNB', () => { + page.setNetworkTo('BNB') + page.checkGasWatcherListingInBNB() + }) it('Copyright & Version', () => { page.checkCopyrightAndVersion() }) diff --git a/cypress/e2e/specs/stake.spec.cy.ts b/cypress/e2e/specs/stake.spec.cy.ts new file mode 100644 index 00000000..a837ba16 --- /dev/null +++ b/cypress/e2e/specs/stake.spec.cy.ts @@ -0,0 +1,19 @@ +import Stake from '../helpers/stake' +const stake = new Stake() + +describe('Testing Entire Site', () => { + describe('Stake', () => { + describe('Before wallet is connected', () => { + before(() => { + stake.visit() + stake.store.verifyReduxStoreSetup('baseEnabled', true) + }) + describe('Stake Layout', () => { + it('Should have the correct title', () => { + stake.checkTitle() + stake.checkDescription() + }) + }) + }) + }) +}) diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 3b9a2766..00bd5d96 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,7 +1,6 @@ { + "extends": "../tsconfig.json", "compilerOptions": { - "types": [ - "@synthetixio/synpress/support", - ] + "types": ["@synthetixio/synpress/support"] } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 101a292e..3447a9b5 100644 --- a/package.json +++ b/package.json @@ -41,15 +41,17 @@ "@bobanetwork/turing-hybrid-compute": "^0.2.0", "@cfx-kit/wallet-avatar": "^0.0.5", "@emotion/styled": "^11.11.0", - "@eth-optimism/core-utils": "0.8.1", + "@eth-optimism/core-utils": "0.13.1", + "@ethersproject/providers": "^5.7.2", "@ethersproject/units": "^5.5.0", "@mui/base": "5.0.0-alpha.72", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.2", "@mui/styles": "^5.3.0", "@sentry/react": "^7.51.0", - "@sentry/tracing": "^7.51.0", - "@walletconnect/web3-provider": "^1.8.0", + "@sentry/tracing": "^7.76.0", + "@walletconnect/ethereum-provider": "^2.10.4", + "@walletconnect/modal": "^2.6.2", "assert": "^2.0.0", "axios": "^0.21.1", "bignumber.js": "^9.0.1", @@ -61,7 +63,7 @@ "dayjs": "^1.11.7", "dotenv": "^8.2.0", "eslint-config-react-app": "^7.0.0", - "ethers": "^5.5.4", + "ethers": "5.7.2", "graphql": "^16.3.0", "html-react-parser": "^4.0.0", "http-browserify": "^1.7.0", @@ -82,9 +84,9 @@ "react-bootstrap-daterangepicker": "^8.0.0", "react-card-flip": "^1.1.5", "react-datepicker": "^4.6.0", - "react-day-picker": "^8.8.0", + "react-day-picker": "^8.9.1", "react-dom": "^17.0.2", - "react-ga4": "^1.4.1", + "react-ga4": "^2.1.0", "react-multi-carousel": "^2.6.5", "react-redux": "^8.0.5", "react-router-dom": "^6.2.1", @@ -94,7 +96,7 @@ "recharts": "^2.1.10", "redux": "^4.1.2", "redux-persist": "^6.0.0", - "redux-thunk": "^2.3.0", + "redux-thunk": "^2.4.2", "sass": "^1.62.1", "serve": "^11.3.2", "stream-browserify": "^3.0.0", @@ -102,27 +104,28 @@ "truncate-middle": "^1.0.6", "ts-md5": "^1.2.11", "typescript": "^5.1.6", - "web3": "^1.8.2", "webpack-bundle-analyzer": "^4.9.1" }, "devDependencies": { "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.5", + "@sentry/types": "^7.73.0", "@storybook/addon-essentials": "^7.0.15", "@storybook/addon-interactions": "^7.0.15", - "@storybook/addon-links": "^7.0.15", + "@storybook/addon-links": "^7.5.2", "@storybook/addon-styling": "^1.0.8", "@storybook/addon-toolbars": "^7.0.17", "@storybook/api": "^7.0.17", - "@storybook/blocks": "^7.0.15", - "@storybook/react": "^7.0.15", - "@storybook/react-webpack5": "^7.0.15", - "@storybook/testing-library": "^0.0.14-next.2", - "@synthetixio/synpress": "^3.7.2-beta.7", + "@storybook/blocks": "^7.5.1", + "@storybook/react": "^7.5.2", + "@storybook/react-webpack5": "^7.5.1", + "@storybook/testing-library": "^0.2.2", + "@synthetixio/synpress": "^3.7.2-beta.8", "@testing-library/cypress": "^10.0.1", - "@testing-library/jest-dom": "^5.16.5", + "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^12.1.2", + "@testing-library/react-hooks": "^8.0.1", "@types/jest": "^29.5.1", "@types/lodash.flatten": "^4.4.7", "@types/lodash.intersection": "^4.4.7", @@ -131,11 +134,11 @@ "@types/lodash.orderby": "^4.6.7", "@types/redux-mock-store": "^1.0.3", "@types/styled-components": "^5.1.26", - "@types/testing-library__jest-dom": "^5.14.6", + "@types/testing-library__jest-dom": "^6.0.0", "@types/truncate-middle": "^1.0.1", "@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/parser": "^5.10.2", - "audit-ci": "^3.1.1", + "audit-ci": "^6.6.1", "cypress": "12.17.3", "env-cmd": "^10.1.0", "eslint-config-prettier": "^8.3.0", @@ -143,11 +146,11 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsdoc": "^46.2.6", "eslint-plugin-prefer-arrow": "^1.2.3", - "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-react": "^7.24.0", "eslint-plugin-storybook": "^0.6.12", "eslint-plugin-testing-library": "^6.0.0", - "eslint-plugin-unicorn": "^32.0.1", + "eslint-plugin-unicorn": "^48.0.1", "husky": "^8.0.3", "jest-styled-components": "^7.1.1", "lint-staged": "^14.0.1", @@ -155,13 +158,26 @@ "prop-types": "^15.8.1", "react-app-rewired": "^2.2.1", "redux-mock-store": "^1.5.4", - "start-server-and-test": "2.0.0", - "storybook": "^7.0.15", + "start-server-and-test": "2.0.1", + "storybook": "^7.5.1", "storybook-addon-sass-postcss": "^0.1.3" }, "jest": { + "moduleNameMapper": { + "@walletconnect/ethereum-provider": "/__mocks__/@walletconnect/ethereum-provider.ts" + }, "collectCoverageFrom": [ - "src/**/*.{ts,tsx}", + "src/components/**/*.tsx", + "src/containers/**/*.tsx", + "src/layout/**/*.tsx", + "src/services/**/*.ts", + "!src/actions/**/*.ts", + "!src/api/**/*.ts", + "!src/selectors/**/*.ts", + "!src/util/**/*.ts", + "!src/reducers/**/*.ts", + "!src/store/**/*.tsx", + "!src/containers/Home/index.tsx", "!src/**/*.stories.{ts,tsx}", "!src/**/types.{ts,tsx}", "!src/**/style.{ts,tsx}" diff --git a/readme.md b/readme.md index 288a6272..deda9dc1 100644 --- a/readme.md +++ b/readme.md @@ -69,10 +69,11 @@ Copy `.env.example` file and name by excluding `.example` and populate the varia | REACT_APP_GA4_MEASUREMENT_ID | Yes | N/A | Google analytics api key | | REACT_APP_SENTRY_DSN | Yes | N/A | Sentry DSN url to catch the error on frontend | | REACT_APP_GAS_POLL_INTERVAL | Yes | 30000 | Poll interval to fetch the gas price and verifier status | -| CYPRESS_REMOTE_DEBUGGING_PORT| Yes | 9222 | Debugging port for Cypress | -| NETWORK_NAME | Yes | goerli | Starting network for wallet | -| SECRET_WORDS | Yes | N/A | Secret phrase for wallet to be used by Cypress e2e test | -| CYPRESS_TEST_HASH | Yes | N/A | Txn hash that has occured on the wallet in the last 6 months | +| REACT_APP_WC_PROJECT_ID= | Yes | N/A | Wallet Connect project ID | +| CYPRESS_REMOTE_DEBUGGING_PORT| Yes | 9222 | Debugging port for Cypress | +| NETWORK_NAME | Yes | goerli | Starting network for wallet | +| SECRET_WORDS | Yes | N/A | Secret phrase for wallet to be used by Cypress e2e test | +| CYPRESS_TEST_HASH | Yes | N/A | Txn hash that has occured on the wallet in the last 6 months | ### To start local dev-server @@ -108,6 +109,22 @@ Run specific tests by giving a path to the file you want to run: $ yarn test ./path-to-file/file.spec.ts ``` +Watch for test file change with coverage report locally at same time. + +```bash + +$ yarn test:w --coverage --collectCoverageFrom= + +``` + +eg. + +```bash + +$ yarn test:w src/components/layout/Footer/GasWatcher/index.test.tsx --coverage --collectCoverageFrom=src/components/layout/Footer/GasWatcher/index.tsx + +``` + ## Measuring test coverage: ```bash $ yarn test:coverage diff --git a/src/actions/bridgeAction.js b/src/actions/bridgeAction.js index a440b675..24b864cd 100644 --- a/src/actions/bridgeAction.js +++ b/src/actions/bridgeAction.js @@ -57,9 +57,22 @@ export function setMultiBridgeMode(mode) { } } -export function setBridgeToAddress(payload) { +export function setBridgeDestinationAddress(payload) { return function (dispatch) { - return dispatch({ type: 'BRIDGE/TOADDRESS/SET', payload }); + return dispatch({type: 'BRIDGE/DESTINATION_ADDRESS/SET', payload }) + } +} + +export function resetBridgeDestinationAddress() { + return function(dispatch) { + return dispatch({type: 'BRIDGE/DESTINATION_ADDRESS/RESET'}); + } +} + +// updates value indicating wether the 'to address' field should be available +export function setBridgeDestinationAddressAvailable(payload) { + return function (dispatch) { + return dispatch({ type: 'BRIDGE/DESTINATION_ADDRESS_AVAILABLE/SET', payload }); } } @@ -99,6 +112,12 @@ export function setTeleportationOfAssetSupported(payload) { } } +export function setTeleportationDestChainId(payload) { + return function (dispatch) { + return dispatch({ type: 'BRIDGE/TELEPORTER/DEST_CHAIN_ID', payload}) + } +} + export function setFetchDepositTxBlock(payload) { store.dispatch({ type: 'BRIDGE/DEPOSIT_TX/BLOCK', payload}) } diff --git a/src/actions/networkAction.js b/src/actions/networkAction.js index 17cd622c..c3c1fc38 100644 --- a/src/actions/networkAction.js +++ b/src/actions/networkAction.js @@ -13,7 +13,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import gasService from 'services/gas.service' import networkService from 'services/networkService' import transactionService from 'services/transaction.service' import { createAction } from './createAction' @@ -22,10 +21,6 @@ export function fetchBalances() { return createAction('BALANCE/GET', () => networkService.getBalances()) } -export function fetchGas() { - return createAction('GAS/GET', () => gasService.getGas()) -} - export function addTokenList() { return createAction('TOKENLIST/GET', () => networkService.addTokenList()) } @@ -55,9 +50,9 @@ export function exitBOBA(token, value) { } //SWAP RELATED -export function depositL1LP(currency, value, decimals) { +export function depositL1LP(currency,value) { return createAction('DEPOSIT/CREATE', () => - networkService.depositL1LP(currency, value, decimals) + networkService.depositL1LP(currency,value) ) } diff --git a/src/actions/setupAction.js b/src/actions/setupAction.js index 6fe1bb1b..0a5cc6d0 100644 --- a/src/actions/setupAction.js +++ b/src/actions/setupAction.js @@ -93,3 +93,10 @@ export function resetChainIdChanged() { return dispatch({ type: 'SETUP/CHAINIDCHANGED/RESET' }) } } + + +export function disconnectSetup() { + return function(dispatch) { + return dispatch({type: 'SETUP/DISCONNECT'}) + } +} \ No newline at end of file diff --git a/src/actions/verifierAction.js b/src/actions/verifierAction.js index 8684c268..a3c4509f 100644 --- a/src/actions/verifierAction.js +++ b/src/actions/verifierAction.js @@ -14,8 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ import verifierService from 'services/verifier.service' -import { createAction } from './createAction' +import {createAction} from './createAction' export function fetchVerifierStatus() { - return createAction('VERIFIER/GET', () => verifierService.getVerifierStatus()) + return createAction('VERIFIER/GET',() => verifierService.getVerifierStatus()) +} + +export function resetVerifierStatus() { + return function(dispatch) { + return dispatch({type: 'VERIFIER/RESET'}); + } } diff --git a/src/app.env.d.ts b/src/app.env.d.ts new file mode 100644 index 00000000..c7dacbd7 --- /dev/null +++ b/src/app.env.d.ts @@ -0,0 +1,9 @@ +/// +import type { MetaMaskInpageProvider } from '@metamask/providers' +import { ExternalProvider } from '@ethersproject/providers' + +declare global { + interface Window { + ethereum?: ExternalProvider | MetaMaskInpageProvider + } +} diff --git a/src/assets/images/bridges/logo/Boringdao-logo-250.png b/src/assets/images/bridges/Boringdao.png similarity index 100% rename from src/assets/images/bridges/logo/Boringdao-logo-250.png rename to src/assets/images/bridges/Boringdao.png diff --git a/src/assets/images/bridges/beamer.png b/src/assets/images/bridges/beamer.png new file mode 100644 index 00000000..828ceacf Binary files /dev/null and b/src/assets/images/bridges/beamer.png differ diff --git a/src/assets/images/bridges/chainswap.png b/src/assets/images/bridges/chainswap.png new file mode 100644 index 00000000..33f125c9 Binary files /dev/null and b/src/assets/images/bridges/chainswap.png differ diff --git a/src/assets/images/bridges/connext.png b/src/assets/images/bridges/connext.png new file mode 100644 index 00000000..47a07a0e Binary files /dev/null and b/src/assets/images/bridges/connext.png differ diff --git a/src/assets/images/bridges/dark/dark_celer.png b/src/assets/images/bridges/dark/dark_celer.png new file mode 100644 index 00000000..d6be135f Binary files /dev/null and b/src/assets/images/bridges/dark/dark_celer.png differ diff --git a/src/assets/images/bridges/dark/dark_rubic_exchange.png b/src/assets/images/bridges/dark/dark_rubic_exchange.png new file mode 100644 index 00000000..f52bfe3d Binary files /dev/null and b/src/assets/images/bridges/dark/dark_rubic_exchange.png differ diff --git a/src/assets/images/bridges/dark/dark_via_protocol.png b/src/assets/images/bridges/dark/dark_via_protocol.png new file mode 100644 index 00000000..0bf00df7 Binary files /dev/null and b/src/assets/images/bridges/dark/dark_via_protocol.png differ diff --git a/src/assets/images/bridges/layerswap.png b/src/assets/images/bridges/layerswap.png new file mode 100644 index 00000000..afc74bcb Binary files /dev/null and b/src/assets/images/bridges/layerswap.png differ diff --git a/src/assets/images/bridges/light/light_celer.png b/src/assets/images/bridges/light/light_celer.png new file mode 100644 index 00000000..177d981a Binary files /dev/null and b/src/assets/images/bridges/light/light_celer.png differ diff --git a/src/assets/images/bridges/light/light_rubic_exchange.png b/src/assets/images/bridges/light/light_rubic_exchange.png new file mode 100644 index 00000000..377eb7df Binary files /dev/null and b/src/assets/images/bridges/light/light_rubic_exchange.png differ diff --git a/src/assets/images/bridges/light/light_via_protocol.png b/src/assets/images/bridges/light/light_via_protocol.png new file mode 100644 index 00000000..e465ffcb Binary files /dev/null and b/src/assets/images/bridges/light/light_via_protocol.png differ diff --git a/src/assets/images/bridges/logo/anyswap-logo-250.png b/src/assets/images/bridges/logo/anyswap-logo-250.png deleted file mode 100644 index 1cb3ac5c..00000000 Binary files a/src/assets/images/bridges/logo/anyswap-logo-250.png and /dev/null differ diff --git a/src/assets/images/bridges/logo/celer-logo-250.png b/src/assets/images/bridges/logo/celer-logo-250.png deleted file mode 100644 index 0852b98f..00000000 Binary files a/src/assets/images/bridges/logo/celer-logo-250.png and /dev/null differ diff --git a/src/assets/images/bridges/logo/polybridge-logo-250.png b/src/assets/images/bridges/logo/polybridge-logo-250.png deleted file mode 100644 index 6c7717f6..00000000 Binary files a/src/assets/images/bridges/logo/polybridge-logo-250.png and /dev/null differ diff --git a/src/assets/images/bridges/logo/symbiosis-logo-250.png b/src/assets/images/bridges/logo/symbiosis-logo-250.png deleted file mode 100644 index 39a73691..00000000 Binary files a/src/assets/images/bridges/logo/symbiosis-logo-250.png and /dev/null differ diff --git a/src/assets/images/bridges/multichain.png b/src/assets/images/bridges/multichain.png new file mode 100644 index 00000000..758987aa Binary files /dev/null and b/src/assets/images/bridges/multichain.png differ diff --git a/src/assets/images/bridges/rango_exchange.png b/src/assets/images/bridges/rango_exchange.png new file mode 100644 index 00000000..617ba250 Binary files /dev/null and b/src/assets/images/bridges/rango_exchange.png differ diff --git a/src/assets/images/bridges/logo/synapse-logo-250.png b/src/assets/images/bridges/synapse.png similarity index 100% rename from src/assets/images/bridges/logo/synapse-logo-250.png rename to src/assets/images/bridges/synapse.png diff --git a/src/assets/images/close.svg b/src/assets/images/close.svg index d54912b7..af0ea237 100644 --- a/src/assets/images/close.svg +++ b/src/assets/images/close.svg @@ -1,3 +1,3 @@ - + diff --git a/src/components/ApplicationBanner/__snapshots__/index.test.tsx.snap b/src/components/ApplicationBanner/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000..66c4dc05 --- /dev/null +++ b/src/components/ApplicationBanner/__snapshots__/index.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApplicationBanner should match snapshot when alerts are enable 1`] = ` + +
+
+
+

+ Message update 1 + + CLICK HERE + +

+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ message two goes here +

+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`ApplicationBanner should match snapshot when empty alerts 1`] = ``; diff --git a/src/components/ApplicationBanner/data.tsx b/src/components/ApplicationBanner/data.tsx new file mode 100644 index 00000000..2a6b19e7 --- /dev/null +++ b/src/components/ApplicationBanner/data.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { BannerText } from './styles' +import { IAppAlert } from './types' + +export const bannerAlerts = (): IAppAlert[] => [ + { + key: 'earn-banner-deprecation', + type: 'warning', + canClose: false, + Component: () => ( + + In preparation for the release of Boba Light Bridge, the Earn program is + being sunset. To withdraw funds, CLICK HERE + + ), + }, +] + +export default bannerAlerts diff --git a/src/components/ApplicationBanner/index.test.tsx b/src/components/ApplicationBanner/index.test.tsx new file mode 100644 index 00000000..db1162d4 --- /dev/null +++ b/src/components/ApplicationBanner/index.test.tsx @@ -0,0 +1,82 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import { BrowserRouter } from 'react-router-dom' +import configureStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import CustomThemeProvider from 'themes' +import { mockLocalStorage, mockedInitialState } from 'util/tests' +import ApplicationBanner from '.' +import { bannerAlerts } from './data' + +jest.mock('./data', () => ({ + bannerAlerts: jest.fn(), +})) + +const mockbannerAlerts = bannerAlerts as jest.MockedFunction< + typeof bannerAlerts +> + +const mockStore = configureStore([thunk]) + +Object.defineProperty(window, 'localStorage', { + value: mockLocalStorage, +}) + +const renderApplicationBanner = () => { + return render( + + + + + + + + ) +} + +describe('ApplicationBanner', () => { + beforeEach(() => { + // @ts-ignore + mockbannerAlerts.mockImplementation(() => [ + { + key: 'message-1', + type: 'warning', + canClose: true, + Component: () => ( +

+ Message update 1 CLICK HERE +

+ ), + }, + { + key: 'message-2', + type: 'warning', + canClose: true, + message: 'message two goes here', + }, + ]) + }) + + test('should match snapshot when empty alerts', () => { + mockbannerAlerts.mockImplementation(() => []) + const { asFragment } = renderApplicationBanner() + expect(asFragment()).toMatchSnapshot() + }) + + test('should match snapshot when alerts are enable', () => { + const { asFragment } = renderApplicationBanner() + expect(asFragment()).toMatchSnapshot() + }) + + test('should update localstorage and ui on clicking close', () => { + renderApplicationBanner() + const closeBtn = screen.getByTestId(`close-icon-message-1`) + expect(closeBtn).toBeVisible() + fireEvent.click(closeBtn) + expect(localStorage.getItem(`appBanner__message-1`)).toEqual( + JSON.stringify(true) + ) + expect(screen.getAllByTestId('banner-item').length).toEqual(1) + }) +}) diff --git a/src/components/ApplicationBanner/index.tsx b/src/components/ApplicationBanner/index.tsx new file mode 100644 index 00000000..b2aaf437 --- /dev/null +++ b/src/components/ApplicationBanner/index.tsx @@ -0,0 +1,69 @@ +import React, { Fragment, useEffect, useState } from 'react' +import { + BannerWrapper, + BannerAction, + BannerContainer, + BannerContent, + BannerText, + CloseIcon, + CloseIconWrapper, +} from './styles' +import { bannerAlerts } from './data' +import { IAppAlert } from './types' + +const ApplicationBanner = () => { + const [alerts, setAlerts] = useState([]) + const [storageChange, setStorageChange] = useState(false) + + useEffect(() => { + const appBanners = bannerAlerts().map((alert) => { + return { + ...alert, + isHidden: JSON.parse( + localStorage.getItem(`appBanner__${alert.key}`) as string + ), + } + }) + setAlerts(appBanners) + }, [storageChange]) + + const onClose = (alertKey: string) => { + localStorage.setItem(`appBanner__${alertKey}`, JSON.stringify(true)) + setStorageChange(!storageChange) + } + + if (alerts && !alerts.length) { + return <> + } + + return ( + + {alerts.map(({ key, canClose, type, message, Component, isHidden }) => { + if (isHidden) { + return + } + return ( + + + {Component ? : {message}} + + {canClose && ( + + { + onClose(key) + }} + > + + + + )} + + ) + })} + + ) +} + +export default ApplicationBanner diff --git a/src/components/ApplicationBanner/styles.ts b/src/components/ApplicationBanner/styles.ts new file mode 100644 index 00000000..84465082 --- /dev/null +++ b/src/components/ApplicationBanner/styles.ts @@ -0,0 +1,99 @@ +import { Svg, Typography } from 'components/global' +import styled from 'styled-components' +import Close from 'assets/images/close.svg' + +export const BannerWrapper = styled('div')` + display: flex; + align-items: center; + width: 100vw; + flex-direction: column; + justify-content: flex-start; + gap: 2px; +` +export const BannerContainer = styled('div')` + position: relative; + transition: max-height 0.4s; + padding: 5px 10px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + width: 100%; + background: ${({ theme: { colors } }) => colors.green[300]}; + color: ${(props) => props.theme.primaryfg}; + + &.open { + max-height: 60px; + } + + @media ${(props) => props.theme.screen.tablet} { + padding: 0 10px; + &.open.expand { + max-height: 120px; + } + } +` +export const BannerContent = styled('div')`` + +export const BannerAction = styled('div')` + cursor: pointer; +` + +export const CloseIconWrapper = styled('div')` + cursor: pointer; + border-radius: 50%; + &:hover { + background: ${({ theme: { name, colors } }) => + name === 'light' ? colors.green[200] : colors.green[100]}; + } + div { + padding: 1px; + height: 24px; + width: 24px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + } +` +export const BannerText = styled(Typography).attrs({ + variant: 'body2', +})` + text-align: center; + max-width: 1440px; + margin: 0 auto; + padding: 10px 0; + + @media ${(props) => props.theme.screen.mobile} { + padding: 5px 10px; + } + + a { + color: #000; + text-decoration: underline; + cursor: pointer; + opacity: 0.65; + text-transform: capitalize; + } +` +export const CloseIcon = styled(Svg).attrs({ + src: Close, + fill: '#fff', +})` + height: 12px; + width: auto; + display: flex; + align-items: center; + justify-content: center; + + div { + display: flex; + } + svg { + max-width: 24px; + min-width: 10px; + height: auto; + stroke: #000; + } +` diff --git a/src/components/ApplicationBanner/types.ts b/src/components/ApplicationBanner/types.ts new file mode 100644 index 00000000..b91181a0 --- /dev/null +++ b/src/components/ApplicationBanner/types.ts @@ -0,0 +1,8 @@ +export interface IAppAlert { + type: 'success' | 'warning' | 'info' + key: string + message?: string + canClose?: boolean + isHidden?: boolean + Component?: React.ElementType<{ className?: string }> +} diff --git a/src/components/SentryWrapper/SentryWrapper.js b/src/components/SentryWrapper/SentryWrapper.js deleted file mode 100644 index 3d005566..00000000 --- a/src/components/SentryWrapper/SentryWrapper.js +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useEffect } from 'react' -import * as Sentry from '@sentry/react'; -import { Typography } from '@mui/material'; -import { APP_ENV, SENTRY_DSN } from 'util/constant'; -import { useSelector } from 'react-redux'; -import { selectActiveNetwork } from 'selectors'; - - -/** - * It's function which wraps compnent and add sentry integration on top of it. - * - * @param {*} children - * @returns wrapp component - */ - -const SentryWrapper = ({ - children -}) => { - - const network = useSelector(selectActiveNetwork()); - - useEffect(() => { - const dns = SENTRY_DSN; - // if no sentry dsn pass don't even initialize. - if (dns) { - // Sentry initializations. - Sentry.init({ - dsn: SENTRY_DSN, - environment: `${APP_ENV}-${network}`, - integrations: [ - new Sentry.Integrations.GlobalHandlers({ - onunhandledrejection: false, /// will avoid to send unhandle browser error. - onerror: false, - }), - // new BrowserTracing() - ], - ignoreErrors: [ - 'top.GLOBALS', //stop sentry to report the random plugin / extensions errors. - // Ignore MM error as we can not control those. - 'Internal JSON-RPC error', - 'JsonRpcEngine', - 'Non-Error promise rejection captured with keys: code' - ], - denyUrls: [ - // Ignore chrome & extensions error - /extensions\//i, - /^chrome:\/\//i, - ], - tracesSampleRate: 1.0, - initialScope: { - tags: { network } - }, - beforeSend: (event, hint) => { - // Avoid sending the sentry events on local env. - if (window.location.hostname === 'localhost') { - return null; - } - - let filterEvent = { - ...event, - breadcrumbs: event.breadcrumbs.filter((b) => b.type !== 'http') /// filter the http stack as it can contain sensity keys - } - - return filterEvent; - } - }) - } - - return () => { - Sentry.close(2000) // to close the sentry client connection on unmounting. - }; - }, [network]); - - - return <> - Something went wrong!}> - {children} - - - -} - - -export default SentryWrapper; diff --git a/src/components/SentryWrapper/SentryWrapper.tsx b/src/components/SentryWrapper/SentryWrapper.tsx new file mode 100644 index 00000000..55298eec --- /dev/null +++ b/src/components/SentryWrapper/SentryWrapper.tsx @@ -0,0 +1,128 @@ +import React, { FC, useEffect } from 'react' +import * as Sentry from '@sentry/react' +import { APP_ENV } from 'util/constant' +import { useSelector } from 'react-redux' +import { selectActiveNetwork, selectActiveNetworkType } from 'selectors' +import { Button, Heading, Typography } from 'components/global' +import styled, { css } from 'styled-components' +import BobaLogoImage from 'assets/images/boba-logo.png' +import CustomThemeProvider from 'themes' +import { mobile } from 'themes/screens' + +export const BobaLogo = styled.div` + width: 50px; + height: 54px; + background: ${`url(${BobaLogoImage}) no-repeat`}; + background-position: 100%; + background-size: contain; + ${mobile(css` + width: 32px; + height: 32px; + `)} +` + +const ContentCenter = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + margin: auto; + width: 300px; + height: 100vh; + align-items: center; + gap: 20px; + backgroundcolor: linear-gradient(180deg, #061122 0 %, #08162c 100 %); +` + +const SentryFallback = () => { + return ( + <> + + + + Opps! + + Something went wrong please try again! + + +
+ +`; + +exports[`BridgeAction AccountEnabled is true should match snapshot 1`] = ` + +
+ +
+
+`; diff --git a/src/containers/Bridging/BridgeAction/index.test.tsx b/src/containers/Bridging/BridgeAction/index.test.tsx new file mode 100644 index 00000000..3721cc41 --- /dev/null +++ b/src/containers/Bridging/BridgeAction/index.test.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import configureStore from 'redux-mock-store' +import { Provider } from 'react-redux' +import { NETWORK, NETWORK_TYPE } from 'util/network/network.util' +import CustomThemeProvider from 'themes' +import BridgeAction from '.' +import thunk from 'redux-thunk' +import { mockedInitialState } from 'util/tests' + +const mockStore = configureStore([thunk]) + +const renderBridgeAction = ({ store }: any) => { + return render( + + + + + + ) +} + +describe('BridgeAction', () => { + let store + + describe('AccountEnabled is true', () => { + let alerts = [ + { text: 'alert1', type: 'info' }, + { text: 'alert2', type: 'info' }, + ] + beforeEach(() => { + store = mockStore({ + ...mockedInitialState, + bridge: { + ...mockedInitialState.bridge, + alerts, + }, + setup: { + ...mockedInitialState.bridge, + accountEnabled: true, + }, + }) + }) + + test('should match snapshot', () => { + const { asFragment } = renderBridgeAction({ + store, + }) + expect(asFragment()).toMatchSnapshot() + }) + + test('should trigger bridgeConfirmModal on click of bridge', () => { + renderBridgeAction({ store }) + const bridgeBtn = screen.getByTestId('bridge-btn') + fireEvent.click(bridgeBtn) + const actions = store.getActions() + expect(actions).toEqual([ + { + destNetworkSelection: undefined, + fast: undefined, + lock: undefined, + payload: 'bridgeConfirmModal', + proposalId: undefined, + selectionLayer: undefined, + token: undefined, + tokenIndex: undefined, + type: 'UI/MODAL/OPEN', + }, + ]) + }) + + test('should not invoke bridgeConfirmModal on click of bridge when bridge action disabled', () => { + alerts = [{ type: 'error', text: 'wrong' }] + store = mockStore({ + ...mockedInitialState, + bridge: { + ...mockedInitialState.bridge, + alerts, + }, + setup: { + ...mockedInitialState.bridge, + accountEnabled: true, + }, + }) + renderBridgeAction({ store }) + const bridgeBtn = screen.getByTestId('bridge-btn') + fireEvent.click(bridgeBtn) + const actions = store.getActions() + expect(actions).toEqual([]) + }) + }) + + describe('AccountEnabled is false', () => { + beforeEach(() => { + store = mockStore({ + ...mockedInitialState, + bridge: { + ...mockedInitialState.bridge, + }, + setup: { + ...mockedInitialState.bridge, + accountEnabled: false, + }, + }) + }) + + test('should match snapshot', () => { + const { asFragment } = renderBridgeAction({ + store, + }) + expect(asFragment()).toMatchSnapshot() + }) + + test('should trigger connect action on click of connect wallet', () => { + renderBridgeAction({ store }) + const connectBtn = screen.getByTestId('connect-btn') + fireEvent.click(connectBtn) + const actions = store.getActions() + expect(actions).toEqual([{ type: 'SETUP/CONNECT', payload: true }]) + }) + }) +}) diff --git a/src/containers/Bridging/BridgeAction/index.tsx b/src/containers/Bridging/BridgeAction/index.tsx new file mode 100644 index 00000000..c02aace9 --- /dev/null +++ b/src/containers/Bridging/BridgeAction/index.tsx @@ -0,0 +1,56 @@ +import { setConnect } from 'actions/setupAction' +import { openModal } from 'actions/uiAction' +import { Heading } from 'components/global' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + selectAccountEnabled, + selectAmountToBridge, + selectBridgeAlerts, + selectTokenToBridge, +} from 'selectors' +import { BridgeActionButton, BridgeActionContainer } from '../styles' + +const BridgeAction = () => { + const dispatch = useDispatch() + const accountEnabled = useSelector(selectAccountEnabled()) + const token = useSelector(selectTokenToBridge()) + const amountToBridge = useSelector(selectAmountToBridge()) + const bridgeAlerts = useSelector(selectBridgeAlerts()) + + const isBridgeActionDisabled = () => { + const hasError = bridgeAlerts.find((alert: any) => alert.type === 'error') + return !token || !amountToBridge || hasError + } + + const onConnect = () => { + dispatch(setConnect(true)) + } + + const onBridge = () => { + if (!isBridgeActionDisabled()) { + dispatch(openModal('bridgeConfirmModal')) + } + } + + return ( + + {!accountEnabled ? ( + Connect Wallet} + /> + ) : ( + Bridge} + /> + )} + + ) +} + +export default BridgeAction diff --git a/src/containers/Bridging/BridgeHeader/__snapshots__/index.test.tsx.snap b/src/containers/Bridging/BridgeHeader/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000..9d0d74af --- /dev/null +++ b/src/containers/Bridging/BridgeHeader/__snapshots__/index.test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeHeader should match snapshot when connected to Mainnet 1`] = ` + +
+

+ Bridge +
+ +
+

+
+ +
+
+
+`; + +exports[`BridgeHeader should match snapshot when connected to TESTNET 1`] = ` + +
+

+ Bridge +
+ +
+

+
+ +
+
+
+`; diff --git a/src/containers/Bridging/BridgeHeader/index.test.tsx b/src/containers/Bridging/BridgeHeader/index.test.tsx new file mode 100644 index 00000000..ca520e57 --- /dev/null +++ b/src/containers/Bridging/BridgeHeader/index.test.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import configureStore from 'redux-mock-store' +import { Provider } from 'react-redux' +import { NETWORK_TYPE } from 'util/network/network.util' +import CustomThemeProvider from 'themes' +import BridgeHeader from '.' +import thunk from 'redux-thunk' +import { mockedInitialState } from 'util/tests' + +const mockStore = configureStore([thunk]) + +const renderBridgeHeader = ({ store }: any) => { + return render( + + + + + + ) +} + +describe('BridgeHeader', () => { + let store + + beforeEach(() => { + store = mockStore(mockedInitialState) + }) + + test('should match snapshot when connected to Mainnet', () => { + const { asFragment } = renderBridgeHeader({ store }) + expect(asFragment()).toMatchSnapshot() + }) + + test('should open setting modal on click of gear icon', () => { + renderBridgeHeader({ store }) + const settingIcon = screen.getByTestId('setting-btn') + fireEvent.click(settingIcon) + const actions = store.getActions() + expect(actions).toEqual([ + { + destNetworkSelection: undefined, + fast: undefined, + lock: undefined, + payload: 'settingsModal', + proposalId: undefined, + selectionLayer: undefined, + token: undefined, + tokenIndex: undefined, + type: 'UI/MODAL/OPEN', + }, + ]) + }) + + test('should open tooltip with correct info', async () => { + renderBridgeHeader({ store }) + const tooltipBtn = screen.getByTestId('tooltip-btn') + fireEvent.mouseEnter(tooltipBtn) + expect(await screen.findByText('Classic Bridge')).toBeInTheDocument() + expect(await screen.findByText('Fast Bridge')).toBeInTheDocument() + }) + + test('should match snapshot when connected to TESTNET', async () => { + store = mockStore({ + ...mockedInitialState, + network: { + ...mockedInitialState.network, + activeNetworkType: NETWORK_TYPE.TESTNET, + }, + }) + const { asFragment } = renderBridgeHeader({ store }) + expect(asFragment()).toMatchSnapshot() + + const tooltipBtn = screen.getByTestId('tooltip-btn') + fireEvent.mouseEnter(tooltipBtn) + expect(await screen.findByText('Classic Bridge')).toBeInTheDocument() + expect(await screen.findByText('Fast Bridge')).toBeInTheDocument() + expect(await screen.findByText('Light Bridge')).toBeInTheDocument() + }) +}) diff --git a/src/containers/Bridging/BridgeHeader/index.tsx b/src/containers/Bridging/BridgeHeader/index.tsx index bc9d8a4c..4adfdcc2 100644 --- a/src/containers/Bridging/BridgeHeader/index.tsx +++ b/src/containers/Bridging/BridgeHeader/index.tsx @@ -1,14 +1,14 @@ import HelpOutlineOutlined from '@mui/icons-material/HelpOutlineOutlined' import { openModal } from 'actions/uiAction' -import { Heading, Typography } from 'components/global' +import { Heading } from 'components/global' import Tooltip from 'components/tooltip/Tooltip' import React from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' +import { selectActiveNetworkType } from 'selectors' import styled, { useTheme } from 'styled-components' +import { NETWORK_TYPE } from 'util/network/network.util' import { BridgeHeaderWrapper, GearIcon, IconWrapper } from './styles' -type Props = {} - export const LabelStyle = styled.span` color: var(--Gray-50, #eee); font-family: Roboto; @@ -27,9 +27,11 @@ export const ValueStyle = styled.span` line-height: 138.3%; ` -const BridgeHeader = (props: Props) => { +const BridgeHeader = () => { const dispatch = useDispatch() const theme: any = useTheme() + const isTestnet = + useSelector(selectActiveNetworkType()) === NETWORK_TYPE.TESTNET const iconColor = theme.name === 'light' ? theme.colors.gray[600] : theme.colors.gray[100] @@ -38,26 +40,63 @@ const BridgeHeader = (props: Props) => { dispatch(openModal('settingsModal')) } + const ClassicBridgeInfo = () => { + return ( + <> + Classic Bridge
+ + Although this option is always available, it takes 7 days to receive + your funds when withdrawing from L2 to L1. + +
+
+ + ) + } + + const FastBridgeInfo = () => { + return ( + <> + Fast Bridge +
+ + A swap-based bridge to Boba L2. This option is only available if the + pool balance is sufficient. + +
+
+ + ) + } + + const LightBridgeInfo = () => { + if (!isTestnet) { + return <> + } + return ( + <> + {' '} + Light Bridge +
+ + Bridge assets instantaneously and even between L2's. This option is + only available for a few selected assets (mostly BOBA). + + + ) + } + return ( Bridge - Classic Bridge
- - Although this option is always available, it takes 7 days to - receive your funds when withdrawing from L2 to L1. - -
-
- Fast Bridge -
- - A swap-based bridge to Boba L2. This option is only available if - the pool balance is sufficient. - + + + } > @@ -70,7 +109,11 @@ const BridgeHeader = (props: Props) => {
- +
) diff --git a/src/containers/Bridging/BridgeInput/BridgeToAddress/__snapshots__/index.test.tsx.snap b/src/containers/Bridging/BridgeInput/BridgeToAddress/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000..d279ce49 --- /dev/null +++ b/src/containers/Bridging/BridgeInput/BridgeToAddress/__snapshots__/index.test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeToAddress should match Snapshot in case of L1 & Classic Bridge 1`] = ` + +
+

+ Destination Address +

+
+ + +
+
+
+`; + +exports[`BridgeToAddress should match Snapshot in case of L2 & Fast Bridge 1`] = ``; diff --git a/src/containers/Bridging/BridgeInput/BridgeToAddress/index.test.tsx b/src/containers/Bridging/BridgeInput/BridgeToAddress/index.test.tsx new file mode 100644 index 00000000..13415325 --- /dev/null +++ b/src/containers/Bridging/BridgeInput/BridgeToAddress/index.test.tsx @@ -0,0 +1,107 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import React from 'react' +import { Provider } from 'react-redux' +import configureStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import CustomThemeProvider from 'themes' +import { mockedInitialState } from 'util/tests' +import BridgeToAddress from '.' + +const mockStore = configureStore([thunk]) + +const renderBridgeToAddress = ({ store }: any) => { + return render( + + + + + + ) +} + +describe('BridgeToAddress', () => { + let store + beforeEach(() => { + store = mockStore({ + ...mockedInitialState, + setup: { + ...mockedInitialState.bridge, + accountEnabled: true, + netLayer: 'L1', + }, + bridge: { + ...mockedInitialState.bridge, + bridgeToAddressState: true, + }, + }) + }) + test('should match Snapshot in case of L1 & Classic Bridge ', () => { + const { asFragment } = renderBridgeToAddress({ + store, + }) + expect(asFragment()).toMatchSnapshot() + }) + + test('Should update state with address on change input', async () => { + renderBridgeToAddress({ store }) + + const input = screen.getByPlaceholderText('Enter destination address') + + fireEvent.change(input, { target: { value: 'RECIEPIENT_ADDRESS' } }) + + const actions = store.getActions() + + expect(actions).toEqual([ + { + payload: 'RECIEPIENT_ADDRESS', + type: 'BRIDGE/DESTINATION_ADDRESS/SET', + }, + ]) + }) + + test('Should update state with address on click of paste button', async () => { + const clipboardMock = { + readText: jest.fn(), + } + ;(global as any).navigator.clipboard = clipboardMock + ;(navigator.clipboard.readText as jest.Mock).mockReturnValue( + 'RECIEPIENT_ADDRESS' + ) + const actions = store.getActions() + renderBridgeToAddress({ store }) + const pasteBtn = screen.getByText('Paste') + await fireEvent.click(pasteBtn) + expect(navigator.clipboard.readText).toHaveBeenCalled() + expect(actions).toEqual([ + { + payload: 'RECIEPIENT_ADDRESS', + type: 'BRIDGE/DESTINATION_ADDRESS/SET', + }, + ]) + ;(navigator.clipboard.readText as jest.Mock).mockReturnValue(0) + store.clearActions() + const newActions = store.getActions() + await fireEvent.click(pasteBtn) + expect(navigator.clipboard.readText).toHaveBeenCalled() + expect(newActions).toEqual([]) + }) + + test('should match Snapshot in case of L2 & Fast Bridge ', () => { + store = mockStore({ + ...mockedInitialState, + setup: { + ...mockedInitialState.bridge, + accountEnabled: true, + netLayer: 'L2', + }, + bridge: { + ...mockedInitialState.bridge, + bridgeToAddressState: true, + }, + }) + const { asFragment } = renderBridgeToAddress({ + store, + }) + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/src/containers/Bridging/BridgeInput/BridgeToAddress/index.tsx b/src/containers/Bridging/BridgeInput/BridgeToAddress/index.tsx index cd6a7823..65ac9d12 100644 --- a/src/containers/Bridging/BridgeInput/BridgeToAddress/index.tsx +++ b/src/containers/Bridging/BridgeInput/BridgeToAddress/index.tsx @@ -1,10 +1,12 @@ import React, { FC, memo, useEffect, useState } from 'react' -import { useSelector } from 'react-redux' +import { useSelector, useDispatch } from 'react-redux' import { - selectBridgeToAddressState, + selectBridgeDestinationAddress, + selectBridgeDestinationAddressAvailable, selectBridgeType, selectLayer, } from 'selectors' +import { setBridgeDestinationAddress } from 'actions/bridgeAction' import { ReceiveContainer } from '../styles' import { Label } from '../../styles' import InputWithButton from 'components/global/inputWithButton' @@ -14,23 +16,27 @@ import { LAYER } from 'util/constant' type Props = {} const BridgeToAddress: FC = ({}) => { - const bridgeToAddressEnable = useSelector(selectBridgeToAddressState()) + const dispatch = useDispatch() + const destinationAddress = useSelector(selectBridgeDestinationAddress()) + const bridgeToAddressEnable = useSelector( + selectBridgeDestinationAddressAvailable() + ) + const layer = useSelector(selectLayer()) const bridgeType = useSelector(selectBridgeType()) - const [toAddress, setToAddress] = useState('') const [isAvailable, setIsAvailable] = useState(true) const onAddressChange = (e: any) => { const text = e.target.value - setToAddress(text) + dispatch(setBridgeDestinationAddress(text)) } const onPaste = async () => { try { const text = await navigator.clipboard.readText() if (text) { - setToAddress(text) + dispatch(setBridgeDestinationAddress(text)) } } catch (err) { // navigator clipboard api not supported in client browser @@ -54,7 +60,7 @@ const BridgeToAddress: FC = ({}) => { { - {layer === LAYER.L2 && bridgeType !== BRIDGE_TYPE.TELEPORTATION ? ( + {layer === LAYER.L2 && bridgeType !== BRIDGE_TYPE.LIGHT ? ( @@ -112,7 +112,7 @@ const Fee = (props: Props) => { +`; + +exports[`Bridge Type Selector should match snapshot when connect to TESTNET and update test correctly on click 1`] = ` + +
+
+ Classic +
+
+ Fast +
+
+ Light +
+
+
+`; diff --git a/src/containers/Bridging/BridgeTypeSelector/index.test.tsx b/src/containers/Bridging/BridgeTypeSelector/index.test.tsx new file mode 100644 index 00000000..30988342 --- /dev/null +++ b/src/containers/Bridging/BridgeTypeSelector/index.test.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import configureStore from 'redux-mock-store' +import { Provider } from 'react-redux' +import { NETWORK, NETWORK_TYPE } from 'util/network/network.util' +import CustomThemeProvider from 'themes' +import BridgeTypeSelector, { BRIDGE_TYPE } from '.' +import thunk from 'redux-thunk' +import { mockedInitialState } from 'util/tests' + +const mockStore = configureStore([thunk]) + +const renderBridgeTypeSelector = ({ store }: any) => { + return render( + + + + + + ) +} + +describe('Bridge Type Selector', () => { + let store + + beforeEach(() => { + store = mockStore({ + ...mockedInitialState, + }) + }) + + test('should match snapshot when connect to MAINNET and trigger class bridge as default', () => { + const { asFragment } = renderBridgeTypeSelector({ store }) + expect(asFragment()).toMatchSnapshot() + const actions = store.getActions() + expect(actions).toEqual([ + { payload: 'CLASSIC', type: 'BRIDGE/TYPE/SELECT' }, + ]) + }) + + test('should match snapshot when connect to TESTNET and update test correctly on click', () => { + store = mockStore({ + ...mockedInitialState, + network: { + activeNetwork: NETWORK.ETHEREUM, + activeNetworkType: NETWORK_TYPE.TESTNET, + }, + }) + + const { asFragment } = renderBridgeTypeSelector({ store }) + expect(asFragment()).toMatchSnapshot() + const lightBtn = screen.getByTestId('light-btn') + + fireEvent.click(lightBtn) + const actions = store.getActions() + expect(actions).toEqual([ + { + payload: 'CLASSIC', + type: 'BRIDGE/TYPE/SELECT', + }, + { + payload: 'LIGHT', + type: 'BRIDGE/TYPE/SELECT', + }, + ]) + }) + + test('should update state on click of each tab', () => { + renderBridgeTypeSelector({ store }) + const classicBtn = screen.getByTestId('classic-btn') + const fastBtn = screen.getByTestId('fast-btn') + const thirdPartyBtn = screen.getByTestId('third-party-btn') + + fireEvent.click(classicBtn) + fireEvent.click(fastBtn) + fireEvent.click(thirdPartyBtn) + const actions = store.getActions() + expect(actions).toEqual([ + { + payload: 'CLASSIC', + type: 'BRIDGE/TYPE/SELECT', + }, + { + payload: 'CLASSIC', + type: 'BRIDGE/TYPE/SELECT', + }, + { + payload: 'FAST', + type: 'BRIDGE/TYPE/SELECT', + }, + { + payload: 'THIRD_PARTY', + type: 'BRIDGE/TYPE/SELECT', + }, + ]) + }) +}) diff --git a/src/containers/Bridging/BridgeTypeSelector/index.tsx b/src/containers/Bridging/BridgeTypeSelector/index.tsx index af05e432..f4024f09 100644 --- a/src/containers/Bridging/BridgeTypeSelector/index.tsx +++ b/src/containers/Bridging/BridgeTypeSelector/index.tsx @@ -1,22 +1,21 @@ -import React from 'react' +import React, { useEffect } from 'react' import { BridgeTabs, BridgeTabItem } from './style' import { useDispatch, useSelector } from 'react-redux' -import { - selectActiveNetworkType, - selectBridgeType, - selectNetworkType, -} from 'selectors' +import { selectActiveNetworkType, selectBridgeType } from 'selectors' import { setBridgeType } from 'actions/bridgeAction' import { NETWORK_TYPE } from '../../../util/network/network.util' export enum BRIDGE_TYPE { CLASSIC = 'CLASSIC', FAST = 'FAST', - TELEPORTATION = 'TELEPORTATION', + LIGHT = 'LIGHT', + THIRD_PARTY = 'THIRD_PARTY', } + const BridgeTypeSelector = () => { const dispatch = useDispatch() const bridgeType = useSelector(selectBridgeType()) + const activeNetworkType = useSelector(selectActiveNetworkType()) // Only show teleportation on testnet for now const isTestnet = @@ -26,15 +25,21 @@ const BridgeTypeSelector = () => { dispatch(setBridgeType(payload)) } + useEffect(() => { + dispatch(setBridgeType(BRIDGE_TYPE.CLASSIC)) + }, [activeNetworkType]) + return ( onTabClick(BRIDGE_TYPE.CLASSIC)} > Classic onTabClick(BRIDGE_TYPE.FAST)} > @@ -43,12 +48,21 @@ const BridgeTypeSelector = () => { {isTestnet ? ( onTabClick(BRIDGE_TYPE.TELEPORTATION)} + data-testid="light-btn" + active={bridgeType === BRIDGE_TYPE.LIGHT} + onClick={() => onTabClick(BRIDGE_TYPE.LIGHT)} + > + Light + + ) : ( + onTabClick(BRIDGE_TYPE.THIRD_PARTY)} > - Now + Third Party - ) : null} + )} ) } diff --git a/src/containers/Bridging/BridgeTypeSelector/style.ts b/src/containers/Bridging/BridgeTypeSelector/style.ts index b5db22b0..2b0d3c40 100644 --- a/src/containers/Bridging/BridgeTypeSelector/style.ts +++ b/src/containers/Bridging/BridgeTypeSelector/style.ts @@ -16,7 +16,7 @@ export const BridgeTabItem = styled.div<{ active?: boolean }>` width: 100%; - padding: 8px 50px; + padding: 8px; text-align: center; font-family: Montserrat; font-style: normal; @@ -40,7 +40,7 @@ export const BridgeTabItem = styled.div<{ &:nth-child(1) { border-radius: 8px 0 0 8px; } - &:nth-child(3) { + &:last-child { border-radius: 0 8px 8px 0; } ` diff --git a/src/containers/Bridging/ThirdPartyBridges/__snapshots__/index.test.tsx.snap b/src/containers/Bridging/ThirdPartyBridges/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000..bbd39e24 --- /dev/null +++ b/src/containers/Bridging/ThirdPartyBridges/__snapshots__/index.test.tsx.snap @@ -0,0 +1,614 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`3rd Party Bridges should match snapshot when network is BNB Mainnet 1`] = ` + +
+

+ Third party bridges +

+
+

+ No bridges available +

+
+
+
+`; + +exports[`3rd Party Bridges should match snapshot when network is ETH Mainnet 1`] = ` + + + +`; + +exports[`3rd Party Bridges should match snapshot when network is TESTNET 1`] = ``; + +exports[`3rd Party Bridges should render icons correctly for light mode 1`] = ` + + + +`; diff --git a/src/containers/Bridging/ThirdPartyBridges/data.ts b/src/containers/Bridging/ThirdPartyBridges/data.ts index 43f32caf..32eca9e5 100644 --- a/src/containers/Bridging/ThirdPartyBridges/data.ts +++ b/src/containers/Bridging/ThirdPartyBridges/data.ts @@ -1,63 +1,93 @@ -import AnyswapLogo from 'assets/images/bridges/logo/anyswap-logo-250.png' -import BoringdaoLogo from 'assets/images/bridges/logo/Boringdao-logo-250.png' -import CelerLogo from 'assets/images/bridges/logo/celer-logo-250.png' -import PolybridgeLogo from 'assets/images/bridges/logo/polybridge-logo-250.png' -import SymbiosisLogo from 'assets/images/bridges/logo/symbiosis-logo-250.png' -import SynapseLogo from 'assets/images/bridges/logo/synapse-logo-250.png' +import BanxaLogo from 'assets/images/bridges/banxa.svg' +import BeamerLogo from 'assets/images/bridges/beamer.png' +import BoringDaoLogo from 'assets/images/bridges/Boringdao.png' +import ChainswapLogo from 'assets/images/bridges/chainswap.png' +import ConnextLogo from 'assets/images/bridges/connext.png' +import LayerswapLogo from 'assets/images/bridges/layerswap.png' +import MultichainLogo from 'assets/images/bridges/multichain.png' +import RangoExchangeLogo from 'assets/images/bridges/rango_exchange.png' +import SynapseLogo from 'assets/images/bridges/synapse.png' + +import CelerLogoDark from 'assets/images/bridges/dark/dark_celer.png' +import RubicExchangeLogoDark from 'assets/images/bridges/dark/dark_rubic_exchange.png' +import ViaProtocolLogoDark from 'assets/images/bridges/dark/dark_via_protocol.png' +import CelerLogoLight from 'assets/images/bridges/light/light_celer.png' +import RubicExchangeLogoLight from 'assets/images/bridges/light/light_rubic_exchange.png' +import ViaProtocolLogoLight from 'assets/images/bridges/light/light_via_protocol.png' export interface IBridges { name: string - icon: any - type: string - link: string - tokens: string[] + icon?: any + iconLight?: any + iconDark?: any + link?: string } export const bobaBridges: IBridges[] = [ { - name: 'Synapse', - icon: SynapseLogo, - type: 'SYNAPSE', - link: 'https://synapseprotocol.com/', - tokens: ['ETH', 'nETH', 'gOHM', 'DAI', 'USDC', 'USDT', 'SYN', 'nUSD'], + name: 'Banxa', + icon: BanxaLogo, + link: 'https://boba.banxa.com/', }, { - name: 'Anyswap', - icon: AnyswapLogo, - type: 'ANYSWAP', - link: 'https://anyswap.exchange/#/router', - tokens: ['MIM', 'AVAX', 'FRAX', 'FTM', 'FXS', 'MATIC'], + name: 'Beamer Bridge', + icon: BeamerLogo, + link: 'https://boba.network/project/beamer-bridge/', + }, + { + name: 'BoringDAO', + icon: BoringDaoLogo, + link: 'https://boba.network/project/boringdao/', }, { name: 'Celer', - icon: CelerLogo, - type: 'CELER', - link: 'https://cbridge.celer.network/#/transfer', - tokens: ['ETH', 'BOBA', 'FRAX', 'OLO'], + icon: null, + iconLight: CelerLogoLight, + iconDark: CelerLogoDark, + link: 'https://boba.network/project/celer/', }, { - name: 'BoringDAO', - icon: BoringdaoLogo, - type: 'BORINGDAO', - link: 'https://oportal.boringdao.com/twoway', - tokens: ['USDT'], + name: 'Chainswap', + icon: ChainswapLogo, + link: 'https://boba.network/project/chainswap/', + }, + { + name: 'Connext', + icon: ConnextLogo, + link: 'https://boba.network/project/connext/', + }, + { + name: 'Layerswap', + icon: LayerswapLogo, + link: 'https://boba.network/project/layerswap-io/', }, { - name: 'PolyBridge', - icon: PolybridgeLogo, - type: 'POLYBRIDGE', - link: 'https://bridge.poly.network/', - tokens: ['BOBA'], + name: 'Multichain', + icon: MultichainLogo, + link: 'https://boba.network/project/multichain/', }, { - name: 'Symbiosis', - icon: SymbiosisLogo, - type: 'SYMBIOSIS', - link: 'https://app.symbiosis.finance/swap', - tokens: ['USDC'], + name: 'Rango Exchange', + icon: RangoExchangeLogo, + link: 'https://boba.network/project/rango-exchange/', + }, + { + name: 'Rubic Exchange', + icon: null, + iconLight: RubicExchangeLogoLight, + iconDark: RubicExchangeLogoDark, + link: 'https://boba.network/project/rubic-exchange/', + }, + { + name: 'Synapse', + icon: SynapseLogo, + link: 'https://boba.network/project/synapse/', + }, + { + name: 'Via Protocol', + icon: null, + iconLight: ViaProtocolLogoLight, + iconDark: ViaProtocolLogoDark, + link: 'https://boba.network/project/via-protocol/', }, ] - -export const bridgeByToken = (symbol: string) => { - return bobaBridges.filter((bridge) => bridge.tokens.includes(symbol)) -} diff --git a/src/containers/Bridging/ThirdPartyBridges/index.test.tsx b/src/containers/Bridging/ThirdPartyBridges/index.test.tsx new file mode 100644 index 00000000..dd5def4f --- /dev/null +++ b/src/containers/Bridging/ThirdPartyBridges/index.test.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import configureStore from 'redux-mock-store' +import { Provider } from 'react-redux' +import { NETWORK, NETWORK_TYPE } from 'util/network/network.util' +import CustomThemeProvider from 'themes' +import ThirdPartyBridges from '.' +import { mockedInitialState } from 'util/tests' + +const mockStore = configureStore() + +const renderThirdPartyBridges = ({ options = {} }: any) => { + return render( + + + + + + ) +} + +describe('3rd Party Bridges', () => { + test('should match snapshot when network is ETH Mainnet', () => { + const { asFragment } = renderThirdPartyBridges({}) + expect(asFragment()).toMatchSnapshot() + }) + + test('should match snapshot when network is BNB Mainnet', () => { + const { asFragment } = renderThirdPartyBridges({ + options: { + network: { + activeNetwork: NETWORK.BNB, + activeNetworkType: NETWORK_TYPE.MAINNET, + }, + }, + }) + expect(asFragment()).toMatchSnapshot() + }) + + test('should match snapshot when network is TESTNET', () => { + const { asFragment } = renderThirdPartyBridges({ + options: { + network: { + activeNetwork: NETWORK.ETHEREUM, + activeNetworkType: NETWORK_TYPE.TESTNET, + }, + }, + }) + expect(asFragment()).toMatchSnapshot() + }) + + test('should render icons correctly for light mode', () => { + const { asFragment } = renderThirdPartyBridges({ + options: { + ui: { + theme: 'light', + }, + }, + }) + expect(asFragment()).toMatchSnapshot() + + expect(screen.getAllByAltText('light-logo', { exact: false }).length).toBe( + 3 + ) + }) + + test('should render bridge list correctly', () => { + renderThirdPartyBridges({}) + expect(screen.getAllByTestId('bridge-item').length).toBe(12) + expect(screen.getByText('Banxa')).toBeInTheDocument() + expect(screen.getByText('Beamer Bridge')).toBeInTheDocument() + expect(screen.getByText('BoringDAO')).toBeInTheDocument() + expect(screen.getByText('Celer')).toBeInTheDocument() + expect(screen.getByText('Chainswap')).toBeInTheDocument() + expect(screen.getByText('Connext')).toBeInTheDocument() + expect(screen.getByText('Layerswap')).toBeInTheDocument() + expect(screen.getByText('Multichain')).toBeInTheDocument() + expect(screen.getByText('Rango Exchange')).toBeInTheDocument() + expect(screen.getByText('Rubic Exchange')).toBeInTheDocument() + expect(screen.getByText('Synapse')).toBeInTheDocument() + expect(screen.getByText('Via Protocol')).toBeInTheDocument() + }) +}) diff --git a/src/containers/Bridging/ThirdPartyBridges/index.tsx b/src/containers/Bridging/ThirdPartyBridges/index.tsx index 173db1cc..dd23c74e 100644 --- a/src/containers/Bridging/ThirdPartyBridges/index.tsx +++ b/src/containers/Bridging/ThirdPartyBridges/index.tsx @@ -1,87 +1,69 @@ -import { Heading } from 'components/global' -import React, { FC, useEffect, useState } from 'react' -import { BridgeItem, BridgeIcon, BridgeLabel, BridgeWrapper } from '../styles' +import React, { FC } from 'react' import { useSelector } from 'react-redux' -import Banxa from 'assets/images/bridges/banxa.svg' +import { selectActiveNetwork, selectActiveNetworkType } from 'selectors' + +import { NETWORK, NETWORK_TYPE } from 'util/network/network.util' +import { SectionLabel } from '../chain/styles' +import { IBridges, bobaBridges } from './data' import { - selectActiveNetwork, - selectActiveNetworkType, - selectTokenToBridge, - selectWalletAddress, -} from 'selectors' -import { NETWORK_TYPE, NETWORK } from 'util/network/network.util' -import { prepareBanxaUrl } from 'util/banxa' -import { IBridges, bridgeByToken } from './data' + BridgeIcon, + BridgeItem, + BridgeLabel, + BridgeList, + ThirdPartyBridgesContainer, +} from './styles' +import { Typography } from 'components/global' +import { useTheme } from 'styled-components' const ThirdPartyBridges: FC = () => { - const token = useSelector(selectTokenToBridge()) const networkType = useSelector(selectActiveNetworkType()) const network = useSelector(selectActiveNetwork()) - const [bridges, setbridges] = useState>([]) - const userWallet = useSelector(selectWalletAddress()) - const [banxaUrl, setBanxaUrl] = useState('') - const [isBanxaEnable, setIsBanxaEnable] = useState(false) - - useEffect(() => { - if (token) { - const _bridges = bridgeByToken(token?.symbol) - setbridges(_bridges) - if (token?.symbol === 'ETH' || token?.symbol === 'BOBA') { - const _banxaUrl = prepareBanxaUrl({ - symbol: token.symbol, - address: userWallet, - }) - setBanxaUrl(_banxaUrl) - setIsBanxaEnable(true) - } - } - }, [token, userWallet]) + const theme: any = useTheme() - if ( - !token || - networkType === NETWORK_TYPE.TESTNET || - network !== NETWORK.ETHEREUM || - (!isBanxaEnable && !bridges.length) - ) { + if (networkType !== NETWORK_TYPE.MAINNET) { return <> } return ( - - Third party bridges - {network === NETWORK.ETHEREUM && isBanxaEnable && ( - - - {`ETH - - Banxa - + + Third party bridges + {network !== NETWORK.ETHEREUM ? ( + + No bridges available + + ) : ( + + {bobaBridges.map((bridge: IBridges) => ( + + + {`${bridge.name} + + {bridge.name} + + ))} + )} - {bridges.map((bridge: IBridges) => ( - - - {`${bridge.name} - - {bridge.name} - - ))} - + ) } diff --git a/src/containers/Bridging/ThirdPartyBridges/styles.ts b/src/containers/Bridging/ThirdPartyBridges/styles.ts new file mode 100644 index 00000000..7fd61e44 --- /dev/null +++ b/src/containers/Bridging/ThirdPartyBridges/styles.ts @@ -0,0 +1,60 @@ +import { Typography } from 'components/global' +import styled, { css } from 'styled-components' + +export const BridgeItem = styled.a` + cursor: pointer; + padding: 16px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + min-height: 64px; + border-radius: 12px; + border: 1px solid + ${({ theme: { colors, name } }) => + name === 'light' ? colors.gray[500] : colors.gray[300]}; + color: ${(props) => props.theme.color}; + text-decoration: none; + &:hover { + background: ${({ theme: { colors, name } }) => + name === 'light' ? colors.gray[500] : colors.gray[300]}; + } +` +export const BridgeIcon = styled.div` + height: 32px; + width: 32px; + border-radius: 50%; +` +export const BridgeLabel = styled(Typography).attrs({ + variant: 'title', +})` + flex: 1; + line-height: normal; + text-transform: capitalize; +` + +export const ThirdPartyBridgesContainer = styled.div` + display: flex; + flex-direction: column; + gap: 14px; + width: 100%; +` +export const BridgeList = styled.div.withConfig({ + shouldForwardProp: (prop) => !['empty'].includes(prop), +})<{ empty?: boolean }>` + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + height: 320px; + overflow-y: scroll; + + ${({ empty, theme: { name, colors } }) => + empty && + css` + display: grid; + justify-content: center; + align-content: center; + color: ${name === 'light' ? colors.gray[700] : colors.gray[100]}; + `} +` diff --git a/src/containers/Bridging/__snapshots__/index.test.tsx.snap b/src/containers/Bridging/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000..6475d8e7 --- /dev/null +++ b/src/containers/Bridging/__snapshots__/index.test.tsx.snap @@ -0,0 +1,653 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Bridging Component should match snapshot on when bridge type is CLASSIC 1`] = ` + +
+
+
+
+

+ Bridge +
+ +
+

+
+ +
+
+
+
+ Classic +
+
+ Fast +
+
+ Third Party +
+
+
+
+

+ From +

+
+
+ + + + + + + + + +
+

+ ethereum +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ To +

+
+
+ + + + + + + + + + + + + +
+

+ boba +

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +`; + +exports[`Bridging Component should match snapshot on when bridge type is THIRD_PARTY 1`] = ` + + + +`; diff --git a/src/containers/Bridging/chain/index.tsx b/src/containers/Bridging/chain/index.tsx index 5c66a3a5..14bd1591 100644 --- a/src/containers/Bridging/chain/index.tsx +++ b/src/containers/Bridging/chain/index.tsx @@ -3,6 +3,8 @@ import { useDispatch, useSelector } from 'react-redux' import { selectActiveNetworkIcon, selectActiveNetworkName, + selectBridgeType, + selectDestChainIdTeleportation, selectLayer, } from 'selectors' import { openModal } from 'actions/uiAction' @@ -21,6 +23,8 @@ import { SwitchChainIcon, SwitchIcon, } from './styles' +import { BRIDGE_TYPE } from '../BridgeTypeSelector' +import { CHAIN_ID_LIST } from '../../../util/network/network.util' type Props = {} @@ -84,6 +88,39 @@ const Chains = (props: Props) => { ) } + const TeleportationDestChainInfo = () => { + const network = CHAIN_ID_LIST[teleportationDestChainId] + if (!network) { + console.warn('TeleportationDestChainInfo: Unknown network id') + return + } + // use correct chain for icons + const NetworkIcon = + NETWORK_ICONS[network?.chain?.toLowerCase()][ + network?.layer?.toUpperCase() + ] + + return ( + <> + + + + + {network?.name || DEFAULT_NETWORK.NAME.L2} + + + ) + } + + const bridgeType = useSelector(selectBridgeType()) + const teleportationDestChainId = useSelector(selectDestChainIdTeleportation()) + + let toChainLabel = + !layer || layer === LAYER.L1 ? : + if (bridgeType === BRIDGE_TYPE.LIGHT && !!teleportationDestChainId) { + // light bridge/teleportation allows for independent network selection + toChainLabel = + } return ( @@ -95,13 +132,17 @@ const Chains = (props: Props) => { - switchChain()}> + switchChain()} id="switchBridgeDirection"> To - openNetworkPicker('l2', true)}> - {!layer || layer === LAYER.L1 ? : } + + openNetworkPicker('l2', bridgeType === BRIDGE_TYPE.LIGHT) + } + > + {toChainLabel} diff --git a/src/containers/Bridging/index.test.tsx b/src/containers/Bridging/index.test.tsx new file mode 100644 index 00000000..82d5bf06 --- /dev/null +++ b/src/containers/Bridging/index.test.tsx @@ -0,0 +1,61 @@ +import { render } from '@testing-library/react' +import useBridgeAlerts from 'hooks/useBridgeAlerts' +import useBridgeCleanup from 'hooks/useBridgeCleanup' +import React from 'react' +import { Provider } from 'react-redux' +import configureStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import CustomThemeProvider from 'themes' +import Bridging from '.' +import { BRIDGE_TYPE } from './BridgeTypeSelector' +import { mockedInitialState } from 'util/tests' + +const mockStore = configureStore([thunk]) + +jest.mock('hooks/useBridgeAlerts') +jest.mock('hooks/useBridgeCleanup') + +const mockUseBridgeAlerts = useBridgeAlerts as jest.MockedFunction< + typeof useBridgeAlerts +> +const mockUseBridgeCleanup = useBridgeCleanup as jest.MockedFunction< + typeof useBridgeCleanup +> + +const renderBridging = ({ store }: any) => { + return render( + + + + + + ) +} + +describe('Bridging Component', () => { + let store: any + + beforeEach(() => { + store = mockStore(mockedInitialState) + }) + + test('should match snapshot on when bridge type is CLASSIC', () => { + const { asFragment } = renderBridging({ store }) + expect(asFragment()).toMatchSnapshot() + expect(mockUseBridgeAlerts).toHaveBeenCalled() + expect(mockUseBridgeCleanup).toHaveBeenCalled() + }) + + test('should match snapshot on when bridge type is THIRD_PARTY', () => { + store = mockStore({ + ...mockedInitialState, + bridge: { + ...mockedInitialState.bridge, + bridgeType: BRIDGE_TYPE.THIRD_PARTY, + }, + }) + + const { asFragment } = renderBridging({ store }) + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/src/containers/Bridging/index.tsx b/src/containers/Bridging/index.tsx index 5c5a747a..5269a174 100644 --- a/src/containers/Bridging/index.tsx +++ b/src/containers/Bridging/index.tsx @@ -1,82 +1,44 @@ -import { setConnect } from 'actions/setupAction' -import { openModal } from 'actions/uiAction' -import { Heading } from 'components/global' -import useBridgeCleanup from 'hooks/useBridgeCleanup' import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { - selectAccountEnabled, - selectAmountToBridge, - selectBridgeAlerts, - selectTokenToBridge, -} from 'selectors' +import { useSelector } from 'react-redux' +import { selectBridgeType } from 'selectors' +import BridgeAction from './BridgeAction' import BridgeAlert from './BridgeAlert' import BridgeHeader from './BridgeHeader' import BridgeInput from './BridgeInput' -import BridgeTypeSelector from './BridgeTypeSelector' +import BridgeTypeSelector, { BRIDGE_TYPE } from './BridgeTypeSelector' import ThirdPartyBridges from './ThirdPartyBridges' import Chains from './chain' -import { - BridgeAction, - BridgeActionButton, - BridgeContent, - BridgeWrapper, - BridginContainer, -} from './styles' +import { BridgeContent, BridgeWrapper, BridginContainer } from './styles' + import useBridgeAlerts from 'hooks/useBridgeAlerts' +import useBridgeCleanup from 'hooks/useBridgeCleanup' const Bridging = () => { useBridgeCleanup() useBridgeAlerts() - const dispatch = useDispatch() - const accountEnabled = useSelector(selectAccountEnabled()) - const token = useSelector(selectTokenToBridge()) - const amountToBridge = useSelector(selectAmountToBridge()) - const bridgeAlerts = useSelector(selectBridgeAlerts()) - - const isBridgeActionDisabled = () => { - const hasError = bridgeAlerts.find((alert: any) => alert.type === 'error') - return !token || !amountToBridge || hasError - } - - const onConnect = () => { - dispatch(setConnect(true)) - } - - const onBridge = () => { - if (isBridgeActionDisabled()) { - return - } - dispatch(openModal('bridgeConfirmModal')) - } + const currentBridgeType = useSelector(selectBridgeType()) return ( - + - - - - - {!accountEnabled ? ( - Connect Wallet} - /> + {currentBridgeType !== BRIDGE_TYPE.THIRD_PARTY ? ( + <> + + + ) : ( - Bridge} - /> + )} - + + {currentBridgeType !== BRIDGE_TYPE.THIRD_PARTY ? ( + + ) : null} - ) } diff --git a/src/containers/Bridging/styles.ts b/src/containers/Bridging/styles.ts index 533a0ae1..f0f2b273 100644 --- a/src/containers/Bridging/styles.ts +++ b/src/containers/Bridging/styles.ts @@ -45,7 +45,7 @@ export const BridgeContent = styled.div` export const BridgeReceiveWrapper = styled.div`` export const BridgeInfo = styled.div`` -export const BridgeAction = styled.div` +export const BridgeActionContainer = styled.div` width: 100%; display: flex; justify-content: around; @@ -71,31 +71,3 @@ export const Label = styled(Typography).attrs({ ? theme.colors.gray[700] : theme.colors.gray[100]}; ` - -export const BridgeItem = styled.a` - padding: 16px; - display: flex; - align-items: center; - justify-content: flex-start; - gap: 8px; - border-radius: 12px; - border: 1px solid ${({ theme }) => theme.colors.gray[300]}; - color: ${(props) => props.theme.color}; - text-decoration: none; - &:hover { - background: ${(props) => props.theme.colors.gray[400]}; - } -` -export const BridgeIcon = styled.div` - height: 32px; - width: 32px; - border-radius: 200px; - background: #fff; -` -export const BridgeLabel = styled(Typography).attrs({ - variant: 'title', -})` - flex: 1; - line-height: normal; - text-transform: capitalize; -` diff --git a/src/containers/dao/OldDao.styles.js b/src/containers/dao/OldDao.styles.js index a8de6a28..dc9df333 100644 --- a/src/containers/dao/OldDao.styles.js +++ b/src/containers/dao/OldDao.styles.js @@ -6,7 +6,7 @@ export const DaoPageContainer = styled.div` display: flex; flex-direction: column; justify-content: space-around; - padding: 10px + padding: 10px; padding-top: 0px; width:100%; max-width:1200px; diff --git a/src/containers/history/History.tsx b/src/containers/history/History.tsx index fb50ea2f..f313a819 100644 --- a/src/containers/history/History.tsx +++ b/src/containers/history/History.tsx @@ -151,7 +151,7 @@ const History = () => { }, POLL_INTERVAL) return ( - + {layer && ( <> diff --git a/src/containers/history/tests/__snapshots__/history.test.tsx.snap b/src/containers/history/tests/__snapshots__/history.test.tsx.snap index c2aaf924..5e402926 100644 --- a/src/containers/history/tests/__snapshots__/history.test.tsx.snap +++ b/src/containers/history/tests/__snapshots__/history.test.tsx.snap @@ -4,6 +4,7 @@ exports[`Testing history page Test History Page 1`] = `
+
+