diff --git a/src/routes/SafeAppLandingPage/SafeAppLandingPage.test.tsx b/src/routes/SafeAppLandingPage/SafeAppLandingPage.test.tsx index 3b93ceddea..371db5898c 100644 --- a/src/routes/SafeAppLandingPage/SafeAppLandingPage.test.tsx +++ b/src/routes/SafeAppLandingPage/SafeAppLandingPage.test.tsx @@ -104,7 +104,12 @@ describe('', () => { expect(screen.queryByText('Available networks')).not.toBeInTheDocument() }) - it('Renders connect wallet button if user wallet is not connected', async () => { + it('Renders connect wallet button if user wallet is not connected and no compatible safe is present in the localstorage', async () => { + // no compatible safes defined in the local storage + storage.setItem('_immortal|v2_RINKEBY__SAFES', {}) + storage.setItem('_immortal|v2_MAINNET__SAFES', {}) + storage.setItem('_immortal|v2_POLYGON__SAFES', {}) + const SHARE_SAFE_APP_LINK = `share/safe-app?appUrl=${SAFE_APP_URL_FROM_MANIFEST}&chainId=${SAFE_APP_CHAIN_ID}` history.push(SHARE_SAFE_APP_LINK) @@ -403,6 +408,53 @@ describe('', () => { ) }) + it('Select a compatible Safe from local storage if no wallet is connected', async () => { + // safes defined in the localstorage + storage.setItem('_immortal|v2_RINKEBY__SAFES', { + [ownedSafe1]: { address: ownedSafe1, chainId: CHAIN_ID.RINKEBY, owners: [ownerAccount] }, + }) + + storage.setItem('_immortal|v2_MAINNET__SAFES', { + [storedSafe]: { address: storedSafe, chainId: CHAIN_ID.ETHEREUM, owners: [ownerAccount] }, + }) + + storage.setItem('_immortal|v2_POLYGON__SAFES', { + [incompatibleSafe]: { address: incompatibleSafe, chainId: CHAIN_ID.POLYGON, owners: [incompatibleSafe] }, + }) + + const customState = { + // no wallet is connected + providers: {}, + addressBook: [ + { + address: ownedSafe1, + name: 'First test Safe', + chainId: CHAIN_ID.RINKEBY, + }, + ], + } + + render(, customState) + + // shows a Loader + const loaderNode = screen.getByRole('progressbar') + expect(loaderNode).toBeInTheDocument() + + // when the Loader is removed check the default safe + await waitForElementToBeRemoved(() => screen.getByRole('progressbar')) + + // safe in the same provided chain as default safe + expect(screen.getByRole('textbox', { hidden: true })).toHaveValue(ownedSafe1) + + // redirect to open the safe app with the compatible Safe is present + const openSafeAppLinkNode = screen.getByText('Connect Safe').closest('a') + + expect(openSafeAppLinkNode).toHaveAttribute( + 'href', + `/rin:${ownedSafe1}/apps?appUrl=${SAFE_APP_URL_FROM_CONFIG_SERVICE}`, + ) + }) + it('Selects a Safe from local storage if no owned safe is returned from service', async () => { // mocked owned safes from service jest.spyOn(safeAppsGatewaySDK, 'getOwnedSafes').mockReturnValue( diff --git a/src/routes/SafeAppLandingPage/SafeAppLandingPage.tsx b/src/routes/SafeAppLandingPage/SafeAppLandingPage.tsx index d53f3d03aa..6b6428b26d 100644 --- a/src/routes/SafeAppLandingPage/SafeAppLandingPage.tsx +++ b/src/routes/SafeAppLandingPage/SafeAppLandingPage.tsx @@ -89,12 +89,13 @@ const SafeAppLandingPage = (): ReactElement => { {/* User Safe Section */} - - + {safeAppChainId && ( + + )} {/* Demo Safe Section */} diff --git a/src/routes/SafeAppLandingPage/components/UserSafeSection.tsx b/src/routes/SafeAppLandingPage/components/UserSafeSection.tsx index 33a2fb7e8f..1f2d46799f 100644 --- a/src/routes/SafeAppLandingPage/components/UserSafeSection.tsx +++ b/src/routes/SafeAppLandingPage/components/UserSafeSection.tsx @@ -2,6 +2,7 @@ import { ReactElement } from 'react' import { useSelector } from 'react-redux' import { Title } from '@gnosis.pm/safe-react-components' import styled from 'styled-components' +import uniq from 'lodash/uniq' import { userAccountSelector } from 'src/logic/wallets/store/selectors' import ConnectButton from 'src/components/ConnectButton' @@ -16,35 +17,65 @@ import UseYourSafe from './UseYourSafe' type UserSafeProps = { safeAppUrl: string availableChains: string[] - safeAppChainId: string | null + safeAppChainId: string +} + +const getCompatibleSafes = ( + safesFromLocalStorage: LocalSafes, + safesFromService: Record, + compatibleChains: string[], + addressBook: AddressBookEntry[], +): AddressBookEntry[] => { + return compatibleChains.reduce((result, chainId) => { + const flatSafesFromLocalStorage = safesFromLocalStorage[chainId]?.map(({ address }) => address) || [] + const flatSafesFromService = safesFromService[chainId] || [] + + // we remove duplicated safes + const allSafes = uniq([...flatSafesFromService, ...flatSafesFromLocalStorage]) + + const compatibleSafes = allSafes.map((address) => ({ + address, + chainId, + name: getNameFromAddressBook(addressBook, address, chainId), + })) + + return [...result, ...compatibleSafes] + }, []) } const UserSafeSection = ({ safeAppUrl, availableChains, safeAppChainId }: UserSafeProps): ReactElement => { const userAddress = useSelector(userAccountSelector) const lastViewedSafeAddress = useSelector(lastViewedSafe) - const ownedSafes = useOwnerSafes() - const localSafes = useLocalSafes() + const safesFromService = useOwnerSafes() + const safesFromLocalStorage = useLocalSafes() const addressBook = useSelector(addressBookState) - const compatibleUserSafes = getCompatibleSafes(ownedSafes, localSafes, availableChains, safeAppChainId, addressBook) + // we include the chainId provided in the query params in the available chains list + const compatibleChains = !availableChains.includes(safeAppChainId) + ? [...availableChains, safeAppChainId] + : availableChains + + // we collect all compatible safes from backend and localstorage + const compatibleSafes = getCompatibleSafes(safesFromLocalStorage, safesFromService, compatibleChains, addressBook) - const selectedUserSafe = getDefaultSafe(compatibleUserSafes, lastViewedSafeAddress, safeAppChainId) + const selectedUserSafe = getDefaultSafe(compatibleSafes, lastViewedSafeAddress, safeAppChainId) const isWalletConnected = !!userAddress + const hasComplatibleSafes = compatibleSafes.length > 0 + + const showConnectWalletSection = !isWalletConnected && !hasComplatibleSafes return ( Use the dApp with your Safe! - {isWalletConnected ? ( - selectedUserSafe ? ( - - ) : ( - - ) - ) : ( + {showConnectWalletSection ? ( + ) : selectedUserSafe ? ( + + ) : ( + )} ) @@ -70,43 +101,6 @@ const ConnectWalletButton = styled(ConnectButton)` height: 52px; ` -const getCompatibleSafes = ( - ownedSafes: Record, - localSafes: LocalSafes, - availableChains: string[], - safeAppChainId: string | null, - addressBook: AddressBookEntry[], -): AddressBookEntry[] => { - // we include the chainId provided in the query params in the available chains list - const compatibleChains = - safeAppChainId && !availableChains.includes(safeAppChainId) ? [...availableChains, safeAppChainId] : availableChains - - // we collect all compatible safes from the Config Service & Local Storage - const compatibleSafes = compatibleChains.reduce((compatibleSafes, chainId) => { - // Safes from Config Service - const safesFromConfigService = - ownedSafes[chainId]?.map((address) => ({ - address, - chainId, - name: getNameFromAddressBook(addressBook, address, chainId), - })) || [] - - // Safes from Local Storage - const safesFromLocalstorage = - localSafes[chainId] - ?.filter(({ address }) => !ownedSafes[chainId]?.includes(address)) // we filter Safes already included - ?.map(({ address }) => ({ - address, - chainId, - name: getNameFromAddressBook(addressBook, address, chainId), - })) || [] - - return [...compatibleSafes, ...safesFromConfigService, ...safesFromLocalstorage] - }, []) - - return compatibleSafes -} - const getNameFromAddressBook = (addressBook: AddressBookEntry[], address: string, chainId: string) => { const addressBookEntry = addressBook.find( (addressBookEntry) => addressBookEntry.address === address && addressBookEntry.chainId === chainId,