diff --git a/packages/ui/cypress/support/commands.ts b/packages/ui/cypress/support/commands.ts index 01870eeb..f9391687 100644 --- a/packages/ui/cypress/support/commands.ts +++ b/packages/ui/cypress/support/commands.ts @@ -3,6 +3,7 @@ import { AuthRequests, Extension, TxRequests } from './Extension' import { MultisigInfo, rejectCurrentMultisigTxs } from '../utils/rejectCurrentMultisigTxs' import { InjectedAccountWitMnemonic } from '../fixtures/injectedAccounts' +import 'cypress-wait-until' // *********************************************** // This example commands.ts shows you how to @@ -67,7 +68,7 @@ Cypress.Commands.add('rejectAuth', (id: number, reason: string) => { }) Cypress.Commands.add('getTxRequests', () => { - return cy.wait(500).then(() => cy.wrap(extension.getTxRequests())) + return cy.wrap(extension.getTxRequests()) }) Cypress.Commands.add('approveTx', (id: number) => { diff --git a/packages/ui/cypress/support/page-objects/landingPage.ts b/packages/ui/cypress/support/page-objects/landingPage.ts index 50c4353d..22b59e12 100644 --- a/packages/ui/cypress/support/page-objects/landingPage.ts +++ b/packages/ui/cypress/support/page-objects/landingPage.ts @@ -1,5 +1,5 @@ export const landingPage = { watchAccountButton: () => cy.get('[data-cy=button-watch-address]'), - accountsOrRpcLoader: () => cy.get('[data-cy="loader-accounts-rpc-connection"]'), + accountsLoader: () => cy.get('[data-cy="loader-accounts-connection"]'), noAccountFoundError: () => cy.get('[data-cy="text-no-account-found"]') } diff --git a/packages/ui/cypress/tests/login.cy.ts b/packages/ui/cypress/tests/login.cy.ts index a9b146da..2bbb2301 100644 --- a/packages/ui/cypress/tests/login.cy.ts +++ b/packages/ui/cypress/tests/login.cy.ts @@ -2,16 +2,18 @@ import { injectedAccounts } from '../fixtures/injectedAccounts' import { landingPageUrl } from '../fixtures/landingData' import { landingPage } from '../support/page-objects/landingPage' import { topMenuItems } from '../support/page-objects/topMenuItems' +import { waitForAuthRequest } from '../utils/waitForAuthRequests' describe('Connect Account', () => { beforeEach(() => { cy.visit(landingPageUrl) cy.initExtension(injectedAccounts) topMenuItems.connectButton().click() - landingPage.accountsOrRpcLoader().should('contain', 'Loading accounts') + landingPage.accountsLoader().should('contain', 'Loading accounts') }) it('Reject connection', () => { + waitForAuthRequest() cy.getAuthRequests().then((authRequests) => { const requests = Object.values(authRequests) // we should have 1 connection request to the extension @@ -26,6 +28,7 @@ describe('Connect Account', () => { }) it('Connects with Alice', () => { + waitForAuthRequest() const AliceAddress = Object.values(injectedAccounts)[0].address cy.getAuthRequests().then((authRequests) => { const requests = Object.values(authRequests) diff --git a/packages/ui/cypress/tests/transactions.cy.ts b/packages/ui/cypress/tests/transactions.cy.ts index a4aa55f7..b3499f95 100644 --- a/packages/ui/cypress/tests/transactions.cy.ts +++ b/packages/ui/cypress/tests/transactions.cy.ts @@ -6,6 +6,8 @@ import { multisigPage } from '../support/page-objects/multisigPage' import { notifications } from '../support/page-objects/notifications' import { sendTxModal } from '../support/page-objects/sendTxModal' import { topMenuItems } from '../support/page-objects/topMenuItems' +import { waitForAuthRequest } from '../utils/waitForAuthRequests' +import { waitForTxRequest } from '../utils/waitForTxRequests' const AliceAddress = Object.values(injectedAccounts)[0].address @@ -20,7 +22,8 @@ describe('Perform transactions', () => { cy.visit(landingPageUrl) cy.initExtension(injectedAccounts) topMenuItems.connectButton().click() - landingPage.accountsOrRpcLoader().should('contain', 'Loading accounts') + landingPage.accountsLoader().should('contain', 'Loading accounts') + waitForAuthRequest() cy.getAuthRequests().then((authRequests) => { const requests = Object.values(authRequests) // we should have 1 connection request to the extension @@ -38,9 +41,9 @@ describe('Perform transactions', () => { multisigPage.newTransactionButton().click() sendTxModal.sendTxTitle().should('be.visible') fillAndSubmitTransactionForm() + waitForTxRequest() cy.getTxRequests().then((req) => { const txRequests = Object.values(req) - console.log('txRequests', JSON.stringify(txRequests)) cy.wrap(txRequests.length).should('eq', 1) cy.wrap(txRequests[0].payload.address).should('eq', AliceAddress) sendTxModal.buttonSend().should('be.disabled') @@ -67,6 +70,7 @@ describe('Perform transactions', () => { multisigPage.newTransactionButton().click() sendTxModal.sendTxTitle().should('be.visible') fillAndSubmitTransactionForm() + waitForTxRequest() cy.getTxRequests().then((req) => { const txRequests = Object.values(req) cy.wrap(txRequests.length).should('eq', 1) diff --git a/packages/ui/cypress/utils/waitForAuthRequests.ts b/packages/ui/cypress/utils/waitForAuthRequests.ts new file mode 100644 index 00000000..33b7cccc --- /dev/null +++ b/packages/ui/cypress/utils/waitForAuthRequests.ts @@ -0,0 +1,2 @@ +export const waitForAuthRequest = () => + cy.waitUntil(() => cy.getAuthRequests().then((req) => Object.entries(req).length > 0)) diff --git a/packages/ui/cypress/utils/waitForTxRequests.ts b/packages/ui/cypress/utils/waitForTxRequests.ts new file mode 100644 index 00000000..10304147 --- /dev/null +++ b/packages/ui/cypress/utils/waitForTxRequests.ts @@ -0,0 +1,2 @@ +export const waitForTxRequest = () => + cy.waitUntil(() => cy.getTxRequests().then((req) => Object.entries(req).length > 0)) diff --git a/packages/ui/package.json b/packages/ui/package.json index 9233b797..a2306242 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -43,6 +43,7 @@ "@typescript-eslint/parser": "^6.7.2", "@vitejs/plugin-react": "^4.0.4", "cypress": "^13.2.0", + "cypress-wait-until": "^2.0.1", "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", "eslint-config-react-app": "^7.0.1", diff --git a/packages/ui/src/components/modals/ChangeMultisig.tsx b/packages/ui/src/components/modals/ChangeMultisig.tsx index ed75fff8..762427e2 100644 --- a/packages/ui/src/components/modals/ChangeMultisig.tsx +++ b/packages/ui/src/components/modals/ChangeMultisig.tsx @@ -31,7 +31,6 @@ import { Button } from '../library' import { ModalCloseButton } from '../library/ModalCloseButton' import { useGetSortAddress } from '../../hooks/useGetSortAddress' import { useGetMultisigAddress } from '../../contexts/useGetMultisigAddress' -import BN from 'bn.js' interface Props { onClose: () => void diff --git a/packages/ui/src/contexts/AccountsContext.tsx b/packages/ui/src/contexts/AccountsContext.tsx index 7579bd16..ec3eba36 100644 --- a/packages/ui/src/contexts/AccountsContext.tsx +++ b/packages/ui/src/contexts/AccountsContext.tsx @@ -24,6 +24,7 @@ export interface IAccountContext { selectedSigner?: Signer allowConnectionToExtension: () => void isAllowedToConnectToExtension: boolean + isLocalStorageSetupDone: boolean } const AccountContext = createContext(undefined) @@ -38,6 +39,7 @@ const AccountContextProvider = ({ children }: AccountContextProps) => { const ownAddressList = useMemo(() => ownAccountList.map((a) => a.address), [ownAccountList]) const [accountGotRequested, setAccountGotRequested] = useState(false) const { chainInfo } = useApi() + const [isLocalStorageSetupDone, setIsLocalStorageSetupDone] = useState(false) // update the current account list with the right network prefix // this will run for every network change useEffect(() => { @@ -67,8 +69,6 @@ const AccountContextProvider = ({ children }: AccountContextProps) => { const getaccountList = useCallback( async (isEthereum: boolean): Promise => { - setIsAccountLoading(true) - web3Enable(DAPP_NAME) .then( (ext) => { @@ -131,6 +131,7 @@ const AccountContextProvider = ({ children }: AccountContextProps) => { if (!accountGotRequested && isAllowedToConnectToExtension) { setAccountGotRequested(true) + setIsAccountLoading(true) // delay the request by 500ms // race condition see https://github.com/polkadot-js/extension/issues/938 setTimeout(() => { @@ -142,9 +143,9 @@ const AccountContextProvider = ({ children }: AccountContextProps) => { getaccountList, isAccountLoading, isAllowedToConnectToExtension, - accountGotRequested, chainInfo, - isExtensionError + isExtensionError, + accountGotRequested ]) useEffect(() => { @@ -153,6 +154,7 @@ const AccountContextProvider = ({ children }: AccountContextProps) => { if (previouslyAllowed === 'true') { setIsAllowedToConnectToExtension(true) } + setIsLocalStorageSetupDone(true) } }, [isAllowedToConnectToExtension]) @@ -181,7 +183,8 @@ const AccountContextProvider = ({ children }: AccountContextProps) => { getAccountByAddress, selectedSigner, allowConnectionToExtension, - isAllowedToConnectToExtension + isAllowedToConnectToExtension, + isLocalStorageSetupDone }} > {children} diff --git a/packages/ui/src/contexts/MultiProxyContext.tsx b/packages/ui/src/contexts/MultiProxyContext.tsx index d40bda4e..4905f7fd 100644 --- a/packages/ui/src/contexts/MultiProxyContext.tsx +++ b/packages/ui/src/contexts/MultiProxyContext.tsx @@ -75,9 +75,11 @@ const MultiProxyContextProvider = ({ children }: MultisigContextProps) => { ), [selectedMultiProxy, ownAddressList] ) + const [isRefreshingMultiProxyList, setIsRefreshingMultiProxyList] = useState(false) const refreshPureToQueryAndMultisigList = useCallback( (data: MultisigsBySignatoriesOrWatchedSubscription | null) => { + setIsRefreshingMultiProxyList(true) // we do have an answer, but there is no multisig if (!!data?.accountMultisigs && data.accountMultisigs.length === 0) { setPureToQuery([]) @@ -123,11 +125,14 @@ const MultiProxyContextProvider = ({ children }: MultisigContextProps) => { // add the selection to the pure to query setPureToQuery(Array.from(pureToQuerySet)) } + + setIsRefreshingMultiProxyList(false) }, [] ) const refreshWatchedPureList = useCallback((data: PureByIdsSubscription | null) => { + setIsRefreshingMultiProxyList(true) const pureProxyMap = new Map>() // we do have an answer, but there is nothing if (!!data?.accounts && data.accounts.length === 0) { @@ -174,6 +179,7 @@ const MultiProxyContextProvider = ({ children }: MultisigContextProps) => { ) setPureProxyList(pureProxyArray) + setIsRefreshingMultiProxyList(false) }, []) const ownAddressIds = useAccountId(ownAddressList) @@ -277,7 +283,7 @@ const MultiProxyContextProvider = ({ children }: MultisigContextProps) => { selectedMultiProxy, multiProxyList, selectMultiProxy, - isLoading: isMultisigsubLoading || isPureSubLoading, + isLoading: isMultisigsubLoading || isPureSubLoading || isRefreshingMultiProxyList, selectedHasProxy, error: isMultisigSubError || isPureSubError, getMultisigByAddress, diff --git a/packages/ui/src/contexts/WalletConnectContext.tsx b/packages/ui/src/contexts/WalletConnectContext.tsx index c3c17d39..93ee4975 100644 --- a/packages/ui/src/contexts/WalletConnectContext.tsx +++ b/packages/ui/src/contexts/WalletConnectContext.tsx @@ -24,7 +24,7 @@ const WalletConnectContextProvider = ({ children }: WalletConnectContextProps) = const core = useMemo( () => new Core({ - logger: 'info', + logger: undefined, // use 'debug' to get more insight projectId: import.meta.env.VITE_WALLETCONNECT_PROJECT_ID // relayUrl: relayerRegionURL ?? import.meta.env.VITE_WALLETCONNECT_PUBLIC_RELAY_URL }), diff --git a/packages/ui/src/contexts/WatchedAddressesContext.tsx b/packages/ui/src/contexts/WatchedAddressesContext.tsx index 1fa3c678..1e74dd0e 100644 --- a/packages/ui/src/contexts/WatchedAddressesContext.tsx +++ b/packages/ui/src/contexts/WatchedAddressesContext.tsx @@ -14,6 +14,7 @@ export interface IWatchedAddressesContext { addWatchedAccount: (address: string) => void removeWatchedAccount: (address: string) => void watchedAddresses: string[] + isInitialized: boolean } const WatchedAddressesContext = createContext(undefined) @@ -84,7 +85,8 @@ const WatchedAddressesContextProvider = ({ children }: WatchedAddressesProps) => value={{ addWatchedAccount, removeWatchedAccount, - watchedAddresses + watchedAddresses, + isInitialized }} > {children} diff --git a/packages/ui/src/hooks/useDisplayError.tsx b/packages/ui/src/hooks/useDisplayError.tsx new file mode 100644 index 00000000..b98753ec --- /dev/null +++ b/packages/ui/src/hooks/useDisplayError.tsx @@ -0,0 +1,70 @@ +import { + HiOutlineArrowTopRightOnSquare as LaunchIcon, + HiOutlineExclamationCircle as ErrorOutlineIcon +} from 'react-icons/hi2' + +import { styled } from '@mui/material/styles' +import { useAccounts } from '../contexts/AccountsContext' +import { Link } from '../components/library' +import { Center } from '../components/layout/Center' +import { useWatchedAddresses } from '../contexts/WatchedAddressesContext' +import { useMultiProxy } from '../contexts/MultiProxyContext' + +export const useDisplayError = () => { + const { isExtensionError, isAccountLoading } = useAccounts() + const { watchedAddresses } = useWatchedAddresses() + const { error: multisigQueryError } = useMultiProxy() + + if (isExtensionError && watchedAddresses.length === 0 && !isAccountLoading) { + return ( + +

+ No account found. Please connect at least one in a wallet extension. More info at{' '} + + wiki.polkadot.network + + +

+
+ ) + } + + if (multisigQueryError) { + return ( + + + +
An error occurred.
+
+
+ ) + } + + return null +} + +const Linkstyled = styled(Link)` + display: inline-flex; + padding-left: 0.2rem; + align-items: center; + + .launchIcon { + margin-left: 0.5rem; + } +` + +const CenterStyled = styled(Center)` + text-align: center; +` + +const ErrorMessageStyled = styled('div')` + text-align: center; + margin-top: 1rem; +` diff --git a/packages/ui/src/hooks/useDisplayLoader.tsx b/packages/ui/src/hooks/useDisplayLoader.tsx new file mode 100644 index 00000000..17291522 --- /dev/null +++ b/packages/ui/src/hooks/useDisplayLoader.tsx @@ -0,0 +1,62 @@ +import { styled } from '@mui/material/styles' +import { useMultiProxy } from '../contexts/MultiProxyContext' +import { useApi } from '../contexts/ApiContext' +import { Box, CircularProgress } from '@mui/material' +import { useNetwork } from '../contexts/NetworkContext' +import { useAccounts } from '../contexts/AccountsContext' +import { useWatchedAddresses } from '../contexts/WatchedAddressesContext' + +export const useDisplayLoader = () => { + const { isLoading: isLoadingMultisigs } = useMultiProxy() + const { api } = useApi() + const { selectedNetworkInfo } = useNetwork() + const { isAccountLoading, isLocalStorageSetupDone } = useAccounts() + const { isInitialized: isWatchAddressInitialized } = useWatchedAddresses() + + if (!isWatchAddressInitialized || !isLocalStorageSetupDone) { + return ( + + + Initialization... + + ) + } + + if (!api) { + return ( + + + {`Connecting to the node at ${selectedNetworkInfo?.rpcUrl}`} + + ) + } + + if (isAccountLoading) { + return ( + + + Loading accounts... + + ) + } + + if (isLoadingMultisigs) { + return ( + + +
Loading your multisigs...
+
+ ) + } + + return null +} + +const LoaderBoxStyled = styled(Box)` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + width: 100%; + padding: 1rem; +` diff --git a/packages/ui/src/hooks/usePendingTx.tsx b/packages/ui/src/hooks/usePendingTx.tsx index 674e1176..54185fca 100644 --- a/packages/ui/src/hooks/usePendingTx.tsx +++ b/packages/ui/src/hooks/usePendingTx.tsx @@ -69,7 +69,7 @@ export const usePendingTx = (multiProxy?: MultiProxy) => { setIsLoading(false) }) .catch(console.error) - }, [api, multisigs]) + }, [api, chainInfo, multisigs]) useEffect(() => { refresh() diff --git a/packages/ui/src/pages/Home/Home.tsx b/packages/ui/src/pages/Home/Home.tsx index c2b5ae83..41fe7871 100644 --- a/packages/ui/src/pages/Home/Home.tsx +++ b/packages/ui/src/pages/Home/Home.tsx @@ -1,23 +1,21 @@ import { PropsWithChildren, useCallback, useEffect, useState } from 'react' -import { Box, CircularProgress, Grid } from '@mui/material' +import { Box, Grid } from '@mui/material' import { useMultiProxy } from '../../contexts/MultiProxyContext' import { useNavigate, useSearchParams } from 'react-router-dom' -import { Button, Link } from '../../components/library' -import { MdErrorOutline as ErrorOutlineIcon } from 'react-icons/md' +import { Button } from '../../components/library' import SuccessCreation from '../../components/SuccessCreation' import NewMulisigAlert from '../../components/NewMulisigAlert' import { styled } from '@mui/material/styles' import { Center } from '../../components/layout/Center' import { useAccounts } from '../../contexts/AccountsContext' import { useWatchedAddresses } from '../../contexts/WatchedAddressesContext' -import { useApi } from '../../contexts/ApiContext' -import { useNetwork } from '../../contexts/NetworkContext' import HeaderView from './HeaderView' import MultisigView from './MultisigView' import TransactionList from '../../components/Transactions/TransactionList' import { ConnectOrWatch } from '../../components/ConnectOrWatch' -import { HiOutlineArrowTopRightOnSquare as LaunchIcon } from 'react-icons/hi2' import { WATCH_ACCOUNT_ANCHOR } from '../Settings/Settings' +import { useDisplayLoader } from '../../hooks/useDisplayLoader' +import { useDisplayError } from '../../hooks/useDisplayError' // import CurrentReferendumBanner from '../../components/CurrentReferendumBanner' interface HomeProps { @@ -29,17 +27,12 @@ const Home = ({ className }: HomeProps) => { const [searchParams, setSearchParams] = useSearchParams({ creationInProgress: 'false' }) - const { isLoading, multiProxyList, error: multisigQueryError } = useMultiProxy() - const { selectedNetworkInfo } = useNetwork() - const { api } = useApi() + const { multiProxyList } = useMultiProxy() const [showNewMultisigAlert, setShowNewMultisigAlert] = useState(false) - const { - isAllowedToConnectToExtension, - isExtensionError, - isAccountLoading, - allowConnectionToExtension - } = useAccounts() + const { isAllowedToConnectToExtension, allowConnectionToExtension } = useAccounts() const { watchedAddresses } = useWatchedAddresses() + const DisplayError = useDisplayError() + const DisplayLoader = useDisplayLoader() const onClosenewMultisigAlert = useCallback(() => { setShowNewMultisigAlert(false) @@ -55,6 +48,14 @@ const Home = ({ className }: HomeProps) => { } }, [onClosenewMultisigAlert, searchParams]) + if (DisplayLoader) { + return DisplayLoader + } + + if (DisplayError) { + return DisplayError + } + if (!isAllowedToConnectToExtension && watchedAddresses.length === 0) { return ( @@ -74,68 +75,6 @@ const Home = ({ className }: HomeProps) => { ) } - if (isExtensionError && watchedAddresses.length === 0 && !isAccountLoading) - return ( - -

- No account found. Please connect at least one in a wallet extension. More info at{' '} - - wiki.polkadot.network - - -

-
- ) - - if (!api || isAccountLoading) { - return ( - - - {isAccountLoading - ? 'Loading accounts...' - : `Connecting to the node at ${selectedNetworkInfo?.rpcUrl}`} - - ) - } - - if (isLoading) { - return ( - - -
Loading your multisigs...
-
- ) - } - - if (multisigQueryError) { - return ( - - - -
An error occurred.
-
-
- ) - } - if (multiProxyList.length === 0) { return ( @@ -226,19 +165,4 @@ const CenterStyled = styled(Center)` text-align: center; ` -const ErrorMessageStyled = styled('div')` - text-align: center; - margin-top: 1rem; -` - -const Linkstyled = styled(Link)` - display: inline-flex; - padding-left: 0.2rem; - align-items: center; - - .launchIcon { - margin-left: 0.5rem; - } -` - export default Home diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 0dcbf80a..76837b76 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -16,7 +16,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "types": ["vite/client", "vite-plugin-svgr/client", "cypress", "node"], + "types": ["vite/client", "vite-plugin-svgr/client", "cypress", "node", "cypress-wait-until"], "paths": { "@polkadot/types/lookup": ["./src/interfaces/types-lookup.ts"], "@polkadot/api/augment": ["./src/interfaces/augment-api.ts"], diff --git a/yarn.lock b/yarn.lock index e85cd630..3cb5ee8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8089,6 +8089,13 @@ __metadata: languageName: node linkType: hard +"cypress-wait-until@npm:^2.0.1": + version: 2.0.1 + resolution: "cypress-wait-until@npm:2.0.1" + checksum: 5ddbd46dff94efbd19ea8fcf35cb6b2488d96df74d662023a620338a8e6d9d8964aa22425d817f86ab6d5cf48ef177a4ccf4c5d1bc901797a8d667272d358dec + languageName: node + linkType: hard + "cypress@npm:^13.2.0": version: 13.2.0 resolution: "cypress@npm:13.2.0" @@ -11492,6 +11499,7 @@ __metadata: "@vitejs/plugin-react": ^4.0.4 "@walletconnect/web3wallet": ^1.9.1 cypress: ^13.2.0 + cypress-wait-until: ^2.0.1 dayjs: ^1.11.10 eslint: ^8.49.0 eslint-config-prettier: ^9.0.0