Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Counterfactual] Display option to refresh balances #3227

Merged
merged 4 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/balances/AssetsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CheckBalance from '@/features/counterfactual/CheckBalance'
import { type ReactElement, useMemo, useContext } from 'react'
import { Button, Tooltip, Typography, SvgIcon, IconButton, Box, Checkbox, Skeleton } from '@mui/material'
import type { TokenInfo } from '@safe-global/safe-gateway-typescript-sdk'
Expand Down Expand Up @@ -257,6 +258,8 @@ const AssetsTable = ({
<EnhancedTable rows={rows} headCells={headCells} />
</div>
)}

<CheckBalance />
</>
)
}
Expand Down
77 changes: 77 additions & 0 deletions src/components/common/CooldownButton/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { render, waitFor } from '@/tests/test-utils'
import CooldownButton from './index'

describe('CooldownButton', () => {
beforeAll(() => {
jest.useFakeTimers()
})

afterAll(() => {
jest.useRealTimers()
})
it('should be disabled initially if startDisabled is set and become enabled after <cooldown> seconds', async () => {
const onClickEvent = jest.fn()
const result = render(
<CooldownButton cooldown={30} onClick={onClickEvent} startDisabled>
Try again
</CooldownButton>,
)

expect(result.getByRole('button')).toBeDisabled()
expect(result.getByText('Try again in 30s')).toBeVisible()

jest.advanceTimersByTime(10_000)

await waitFor(() => {
expect(result.getByRole('button')).toBeDisabled()
expect(result.getByText('Try again in 20s')).toBeVisible()
})

jest.advanceTimersByTime(5_000)

await waitFor(() => {
expect(result.getByRole('button')).toBeDisabled()
expect(result.getByText('Try again in 15s')).toBeVisible()
})

jest.advanceTimersByTime(15_000)

await waitFor(() => {
expect(result.getByRole('button')).toBeEnabled()
})
result.getByRole('button').click()

expect(onClickEvent).toHaveBeenCalledTimes(1)
await waitFor(() => {
expect(result.getByRole('button')).toBeDisabled()
})
})

it('should be enabled initially if startDisabled is not set and become disabled after click', async () => {
const onClickEvent = jest.fn()
const result = render(
<CooldownButton cooldown={30} onClick={onClickEvent}>
Try again
</CooldownButton>,
)

expect(result.getByRole('button')).toBeEnabled()
result.getByRole('button').click()

expect(onClickEvent).toHaveBeenCalledTimes(1)

await waitFor(() => {
expect(result.getByRole('button')).toBeDisabled()
expect(result.getByText('Try again in 30s')).toBeVisible()
})

jest.advanceTimersByTime(30_000)

await waitFor(() => {
expect(result.getByRole('button')).toBeEnabled()
})
result.getByRole('button').click()

expect(onClickEvent).toHaveBeenCalledTimes(2)
})
})
48 changes: 48 additions & 0 deletions src/components/common/CooldownButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Button } from '@mui/material'
import { useState, useCallback, useEffect, type ReactNode } from 'react'

// TODO: Extract into a hook so it can be reused for links and not just buttons
const CooldownButton = ({
onClick,
cooldown,
startDisabled = false,
children,
}: {
onClick: () => void
startDisabled?: boolean
cooldown: number // Cooldown in seconds
children: ReactNode
}) => {
const [remainingSeconds, setRemainingSeconds] = useState(startDisabled ? cooldown : 0)
const [lastSendTime, setLastSendTime] = useState(startDisabled ? Date.now() : 0)

const adjustSeconds = useCallback(() => {
const remainingCoolDownSeconds = Math.max(0, cooldown * 1000 - (Date.now() - lastSendTime)) / 1000
setRemainingSeconds(remainingCoolDownSeconds)
}, [cooldown, lastSendTime])

useEffect(() => {
// Counter for progress
const interval = setInterval(adjustSeconds, 1000)
return () => clearInterval(interval)
}, [adjustSeconds])

const handleClick = () => {
setLastSendTime(Date.now())
setRemainingSeconds(cooldown)
onClick()
}

const isDisabled = remainingSeconds > 0

return (
<Button onClick={handleClick} variant="contained" size="small" disabled={isDisabled}>
<span>
{children}
{remainingSeconds > 0 && ` in ${Math.floor(remainingSeconds)}s`}
</span>
</Button>
)
}

export default CooldownButton
43 changes: 43 additions & 0 deletions src/features/counterfactual/CheckBalance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import CooldownButton from '@/components/common/CooldownButton'
import ExternalLink from '@/components/common/ExternalLink'
import { getCounterfactualBalance } from '@/features/counterfactual/utils'
import { useCurrentChain } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useWeb3 } from '@/hooks/wallets/web3'
import { getBlockExplorerLink } from '@/utils/chains'
import { Box, Typography } from '@mui/material'

const CheckBalance = () => {
const { safe, safeAddress } = useSafeInfo()
const chain = useCurrentChain()
const provider = useWeb3()

if (safe.deployed) return null

const handleBalanceRefresh = async () => {
void getCounterfactualBalance(safeAddress, provider, chain, true)
}

const blockExplorerLink = chain ? getBlockExplorerLink(chain, safeAddress) : undefined

return (
<Box textAlign="center" p={3}>
<Typography variant="h4" fontWeight="bold" mb={2}>
Don&apos;t see your tokens?
</Typography>
<Box display="flex" flexDirection="column" gap={1} alignItems="center">
<CooldownButton cooldown={15} onClick={handleBalanceRefresh}>
Refresh balance
</CooldownButton>
<Typography>or</Typography>
{blockExplorerLink && (
<Typography>
check on <ExternalLink href={blockExplorerLink.href}>Block Explorer</ExternalLink>
</Typography>
)}
</Box>
</Box>
)
}

export default CheckBalance
12 changes: 9 additions & 3 deletions src/features/counterfactual/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,14 @@ export const deploySafeAndExecuteTx = async (
return dispatchTxExecutionAndDeploySafe(safeTx, txOptions, onboard, chainId, onSuccess)
}

const { getStore: getNativeBalance, setStore: setNativeBalance } = new ExternalStore<bigint | undefined>()
export const { getStore: getNativeBalance, setStore: setNativeBalance } = new ExternalStore<bigint | undefined>()

export const getCounterfactualBalance = async (safeAddress: string, provider?: BrowserProvider, chain?: ChainInfo) => {
export const getCounterfactualBalance = async (
safeAddress: string,
provider?: BrowserProvider,
chain?: ChainInfo,
ignoreCache?: boolean,
) => {
let balance: bigint | undefined

if (!chain) return undefined
Expand All @@ -133,7 +138,8 @@ export const getCounterfactualBalance = async (safeAddress: string, provider?: B
balance = await provider.getBalance(safeAddress)
} else {
const cachedBalance = getNativeBalance()
balance = cachedBalance !== undefined ? cachedBalance : await getWeb3ReadOnly()?.getBalance(safeAddress)
balance =
cachedBalance !== undefined && !ignoreCache ? cachedBalance : await getWeb3ReadOnly()?.getBalance(safeAddress)
setNativeBalance(balance)
}

Expand Down
Loading