Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
feat: Show compatible Safes from localstorage when a wallet has not b…
Browse files Browse the repository at this point in the history
…een connected (#3924)

* Show compatible safes from localstorage when a wallet has not been connected
  • Loading branch information
DaniSomoza authored Jun 7, 2022
1 parent e1531fc commit 35494d6
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 56 deletions.
54 changes: 53 additions & 1 deletion src/routes/SafeAppLandingPage/SafeAppLandingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ describe('<SafeAppLandingPage>', () => {
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)
Expand Down Expand Up @@ -403,6 +408,53 @@ describe('<SafeAppLandingPage>', () => {
)
})

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(<SafeAppLandingPage />, 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(
Expand Down
13 changes: 7 additions & 6 deletions src/routes/SafeAppLandingPage/SafeAppLandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ const SafeAppLandingPage = (): ReactElement => {

<ActionsContainer>
{/* User Safe Section */}
<UserSafeSection
safeAppUrl={safeAppUrl}
availableChains={availableChains}
safeAppChainId={safeAppChainId}
/>

{safeAppChainId && (
<UserSafeSection
safeAppUrl={safeAppUrl}
availableChains={availableChains}
safeAppChainId={safeAppChainId}
/>
)}
{/* Demo Safe Section */}
<TryDemoSafe safeAppUrl={safeAppUrl} />
</ActionsContainer>
Expand Down
92 changes: 43 additions & 49 deletions src/routes/SafeAppLandingPage/components/UserSafeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<string, string[]>,
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 (
<UserSafeContainer>
<Title size="xs">Use the dApp with your Safe!</Title>
{isWalletConnected ? (
selectedUserSafe ? (
<UseYourSafe safeAppUrl={safeAppUrl} defaultSafe={selectedUserSafe} safes={compatibleUserSafes} />
) : (
<CreateNewSafe safeAppUrl={safeAppUrl} />
)
) : (
{showConnectWalletSection ? (
<ConnectWalletContainer>
<ConnectWalletButton data-testid="connect-wallet-btn" />
</ConnectWalletContainer>
) : selectedUserSafe ? (
<UseYourSafe safeAppUrl={safeAppUrl} defaultSafe={selectedUserSafe} safes={compatibleSafes} />
) : (
<CreateNewSafe safeAppUrl={safeAppUrl} />
)}
</UserSafeContainer>
)
Expand All @@ -70,43 +101,6 @@ const ConnectWalletButton = styled(ConnectButton)`
height: 52px;
`

const getCompatibleSafes = (
ownedSafes: Record<string, string[]>,
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,
Expand Down

0 comments on commit 35494d6

Please sign in to comment.