Share accounts (#539)

* feat: do not redirect to wallet on portfolio page

* fix: use connected wallet for AccountMenu

* fix: fixed ghost AccountDetails

* feat: created ShareBar and share functionality

* fix: don’t show shareBar if no address is present

* fix: stupid 'next/navigation'

* tidy: format

* fix: fixed tests

*  routing and pages for HLS (#538)

* 🐛 use useAccountIds

* fix: fixed the tests

* fix: accountIds is now a suspense

---------

Co-authored-by: Bob van der Helm <34470358+bobthebuidlr@users.noreply.github.com>
This commit is contained in:
Linkie Link 2023-10-13 13:49:38 +02:00 committed by GitHub
parent 4915729ed5
commit ea614997a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 140 additions and 36 deletions

View File

@ -14,15 +14,28 @@ jest.mock('hooks/useHealthComputer', () =>
jest.mock('components/Account/AccountBalancesTable', () => jest.fn(() => null))
const mockedUseCurrentAccount = useCurrentAccount as jest.Mock
const mockedAccount = { id: '1', deposits: [], lends: [], debts: [], vaults: [] }
const mockedAccounts = [
{ id: '1', deposits: [], lends: [], debts: [], vaults: [] },
{ id: '2', deposits: [], lends: [], debts: [], vaults: [] },
]
jest.mock('hooks/useAccountId', () => jest.fn(() => '1'))
jest.mock('hooks/useAccounts', () => jest.fn(() => [mockedAccount]))
jest.mock('hooks/useAccounts', () =>
jest.fn(() => ({
data: mockedAccounts,
})),
)
jest.mock('hooks/useAccountIds', () =>
jest.fn(() => ({
data: ['1', '2'],
})),
)
jest.mock('hooks/useCurrentAccount', () => jest.fn(() => mockedAccounts[0]))
describe('<AccountDetails />', () => {
beforeAll(() => {
useStore.setState({
address: 'walletAddress',
accounts: [mockedAccount],
accounts: mockedAccounts,
})
})
@ -31,7 +44,7 @@ describe('<AccountDetails />', () => {
})
it('renders account details WHEN account is selected', () => {
mockedUseCurrentAccount.mockReturnValue(mockedAccount)
mockedUseCurrentAccount.mockReturnValue(mockedAccounts)
render(<AccountDetails />)
const container = screen.queryByTestId('account-details')

View File

@ -5,8 +5,8 @@ export default async function getAccountIds(
address?: string,
previousResults?: string[],
): Promise<string[]> {
if (!address) return []
try {
if (!address) return []
const accountNftQueryClient = await getAccountNftQueryClient()
const lastItem = previousResults && previousResults.at(-1)

View File

@ -17,6 +17,7 @@ import { DEFAULT_SETTINGS } from 'constants/defaultSettings'
import { REDUCE_MOTION_KEY } from 'constants/localStore'
import { ORACLE_DENOM } from 'constants/oracle'
import useAccountId from 'hooks/useAccountId'
import useAccountIds from 'hooks/useAccountIds'
import useAccounts from 'hooks/useAccounts'
import useBorrowMarketAssetsTableData from 'hooks/useBorrowMarketAssetsTableData'
import useCurrentAccount from 'hooks/useCurrentAccount'
@ -34,12 +35,15 @@ import {
export default function AccountDetailsController() {
const address = useStore((s) => s.address)
const { isLoading } = useAccounts(address)
const { data: accounts, isLoading } = useAccounts(address)
const { data: accountIds } = useAccountIds(address, false)
const accountId = useAccountId()
const account = useCurrentAccount()
const focusComponent = useStore((s) => s.focusComponent)
if (!address || focusComponent) return null
const focusComponent = useStore((s) => s.focusComponent)
const isOwnAccount = accountId && accountIds?.includes(accountId)
if (!address || focusComponent || !isOwnAccount) return null
if ((isLoading && accountId && !focusComponent) || !account) return <Skeleton />

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'
import { useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import AccountStats from 'components/Account/AccountList/AccountStats'
import Card from 'components/Card'
@ -26,7 +26,7 @@ export default function AccountList(props: Props) {
const navigate = useNavigate()
const { pathname } = useLocation()
const currentAccountId = useAccountId()
const { address } = useParams()
const address = useStore((s) => s.address)
const { data: accountIds } = useAccountIds(address)
useEffect(() => {
@ -37,7 +37,7 @@ export default function AccountList(props: Props) {
}
}, [currentAccountId])
if (!accountIds?.length) return null
if (!accountIds || !accountIds.length) return null
return (
<div className='flex flex-wrap w-full p-4'>

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'
import { useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
import AccountFund from 'components/Account/AccountFund/AccountFundFullPage'
@ -30,7 +30,7 @@ const ACCOUNT_MENU_BUTTON_ID = 'account-menu-button'
export default function AccountMenuContent() {
const navigate = useNavigate()
const { pathname } = useLocation()
const { address } = useParams()
const address = useStore((s) => s.address)
const { data: accountIds } = useAccountIds(address)
const accountId = useAccountId()
@ -42,8 +42,9 @@ export default function AccountMenuContent() {
const [lendAssets] = useLocalStorage<boolean>(LEND_ASSETS_KEY, DEFAULT_SETTINGS.lendAssets)
const { enableAutoLendAccountId } = useAutoLend()
const hasCreditAccounts = !!accountIds.length
const isAccountSelected = isNumber(accountId)
const hasCreditAccounts = !!accountIds?.length
const isAccountSelected =
hasCreditAccounts && accountId && isNumber(accountId) && accountIds.includes(accountId)
const checkHasFunds = useCallback(() => {
return (

View File

@ -23,7 +23,7 @@ export default function ActionButton(props: ButtonProps) {
if (!address) return <WalletConnectButton {...defaultProps} />
if (accountIds.length === 0) {
if (accountIds && accountIds.length === 0) {
return (
<Button
onClick={handleCreateAccountClick}

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 512 512">
<path
fill="currentColor"
d="M459.654,233.373l-90.531,90.5c-49.969,50-131.031,50-181,0c-7.875-7.844-14.031-16.688-19.438-25.813 l42.063-42.063c2-2.016,4.469-3.172,6.828-4.531c2.906,9.938,7.984,19.344,15.797,27.156c24.953,24.969,65.563,24.938,90.5,0 l90.5-90.5c24.969-24.969,24.969-65.563,0-90.516c-24.938-24.953-65.531-24.953-90.5,0l-32.188,32.219 c-26.109-10.172-54.25-12.906-81.641-8.891l68.578-68.578c50-49.984,131.031-49.984,181.031,0 C509.623,102.342,509.623,183.389,459.654,233.373z M220.326,382.186l-32.203,32.219c-24.953,24.938-65.563,24.938-90.516,0 c-24.953-24.969-24.953-65.563,0-90.531l90.516-90.5c24.969-24.969,65.547-24.969,90.5,0c7.797,7.797,12.875,17.203,15.813,27.125 c2.375-1.375,4.813-2.5,6.813-4.5l42.063-42.047c-5.375-9.156-11.563-17.969-19.438-25.828c-49.969-49.984-131.031-49.984-181.016,0 l-90.5,90.5c-49.984,50-49.984,131.031,0,181.031c49.984,49.969,131.031,49.969,181.016,0l68.594-68.594 C274.561,395.092,246.42,392.342,220.326,382.186z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,7 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 120 120">
<path
fill="currentColor"
d="M0.3,3.8l46.3,61.9L0,116.2h10.5l40.8-44.1l33,44.1H120L71.1,50.7l43.4-46.9H104L66.4,44.5L36,3.8
H0.3z M15.7,11.6h16.4l72.4,96.9H88.2L15.7,11.6z"
/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@ -7,6 +7,7 @@ export { default as ArrowCircledTopRight } from 'components/Icons/ArrowCircledTo
export { default as ArrowDownLine } from 'components/Icons/ArrowDownLine.svg'
export { default as ArrowRight } from 'components/Icons/ArrowRight.svg'
export { default as ArrowUpLine } from 'components/Icons/ArrowUpLine.svg'
export { default as Chain } from 'components/Icons/Chain.svg'
export { default as Check } from 'components/Icons/Check.svg'
export { default as CheckCircled } from 'components/Icons/CheckCircled.svg'
export { default as ChevronDown } from 'components/Icons/ChevronDown.svg'
@ -54,6 +55,7 @@ export { default as SwapIcon } from 'components/Icons/SwapIcon.svg'
export { default as ThreeDots } from 'components/Icons/ThreeDots.svg'
export { default as TooltipArrow } from 'components/Icons/TooltipArrow.svg'
export { default as TrashBin } from 'components/Icons/TrashBin.svg'
export { default as Twitter } from 'components/Icons/Twitter.svg'
export { default as VerticalThreeLine } from 'components/Icons/VerticalThreeLine.svg'
export { default as Wallet } from 'components/Icons/Wallet.svg'
// @endindex

View File

@ -1,5 +1,4 @@
import classNames from 'classnames'
import { useParams } from 'react-router-dom'
import { menuTree } from 'components/Header/DesktopHeader'
import { Logo } from 'components/Icons'
@ -9,7 +8,7 @@ import useStore from 'store'
import { getRoute } from 'utils/route'
export default function DesktopNavigation() {
const { address } = useParams()
const address = useStore((s) => s.address)
const accountId = useAccountId()
const focusComponent = useStore((s) => s.focusComponent)
@ -26,7 +25,7 @@ export default function DesktopNavigation() {
)}
>
<NavLink href={getRoute('trade', address, accountId)}>
<span className='block h-10 w-10'>
<span className='block w-10 h-10'>
<Logo className='text-white' />
</span>
</NavLink>

View File

@ -1,6 +1,6 @@
import classNames from 'classnames'
import React, { ReactNode, useMemo } from 'react'
import { NavLink } from 'react-router-dom'
import { ReactNode, useMemo } from 'react'
import { NavLink, useParams } from 'react-router-dom'
import { FormattedNumber } from 'components/FormattedNumber'
import Loading from 'components/Loading'
@ -30,6 +30,7 @@ interface Props {
export default function PortfolioCard(props: Props) {
const { data: account } = useAccount(props.accountId)
const { health } = useHealthComputer(account)
const { address: urlAddress } = useParams()
const { data: prices } = usePrices()
const currentAccountId = useAccountId()
const { allAssets: lendingAssets } = useLendingMarketAssetsTableData()
@ -91,7 +92,7 @@ export default function PortfolioCard(props: Props) {
return (
<NavLink
to={getRoute(`portfolio/${props.accountId}` as Page, address, currentAccountId)}
to={getRoute(`portfolio/${props.accountId}` as Page, urlAddress, currentAccountId)}
className={classNames('w-full hover:bg-white/5', !reduceMotion && 'transition-all')}
>
<Skeleton

View File

@ -42,7 +42,7 @@ export default function AccountSummary() {
if (!walletAddress && !urlAddress) return <ConnectInfo />
if (!isLoading && accountIds?.length === 0) {
if (!isLoading && accountIds && accountIds.length === 0) {
return (
<Card
className='w-full h-fit bg-white/5'
@ -72,7 +72,7 @@ export default function AccountSummary() {
<div
className={classNames('grid w-full grid-cols-1 gap-6', 'md:grid-cols-2', 'lg:grid-cols-3')}
>
{accountIds.map((accountId: string, index: number) => {
{accountIds?.map((accountId: string, index: number) => {
return <PortfolioCard key={accountId} accountId={accountId} />
})}
</div>

View File

@ -4,17 +4,18 @@ import Intro from 'components/Intro'
import useStore from 'store'
export default function PortfolioIntro() {
const { address } = useParams()
const walletAddress = useStore((s) => s.address)
const { address: urlAddress } = useParams()
const address = useStore((s) => s.address)
const isCurrentWalllet = !urlAddress || urlAddress === address
return (
<Intro
text={
address && !walletAddress ? (
!isCurrentWalllet ? (
<>
This is the <span className='text-white'>Portfolio</span> of the address{' '}
<span className='text-white'>{address}</span>. You can see all Credit Accounts of this
address, but you can&apos;t interact with them.
<span className='text-white'>{urlAddress}</span>. You can see all Credit Accounts of
this address, but you can&apos;t interact with them.
</>
) : (
<>

View File

@ -0,0 +1,55 @@
import classNames from 'classnames'
import { useLocation, useParams } from 'react-router-dom'
import useClipboard from 'react-use-clipboard'
import Button from 'components/Button'
import { Chain, Check, Twitter } from 'components/Icons'
import Text from 'components/Text'
import { Tooltip } from 'components/Tooltip'
import ConditionalWrapper from 'hocs/ConditionalWrapper'
import { DocURL } from 'types/enums/docURL'
interface Props {
text: string
}
export default function ShareBar(props: Props) {
const { address } = useParams()
const { pathname } = useLocation()
const currentUrl = `https://${location.host}${pathname}`
const [isCopied, setCopied] = useClipboard(currentUrl, {
successDuration: 1000 * 5,
})
if (!window || !address) return null
return (
<div className='flex justify-end w-full gap-4'>
<ConditionalWrapper
condition={isCopied}
wrapper={(children) => (
<Tooltip type='info' content={<Text size='2xs'>Link copied!</Text>}>
{children}
</Tooltip>
)}
>
<Button
color='secondary'
iconClassName='w-4 h-4'
className={classNames('!p-2', isCopied && '!bg-transparent')}
leftIcon={isCopied ? <Check /> : <Chain />}
onClick={setCopied}
/>
</ConditionalWrapper>
<Button
color='secondary'
iconClassName='w-4 h-4'
className='!p-2'
leftIcon={<Twitter />}
onClick={(e) => {
e.preventDefault()
window.open(`${DocURL.X_SHARE_URL}?text=${props.text} ${currentUrl}`, '_blank')
}}
/>
</div>
)
}

View File

@ -224,7 +224,8 @@ export class OsmosisTheGraphDataFeed implements IDatafeedChartApi {
}
const filler = Array.from({ length: this.batchSize - bars.length }).map((_, index) => ({
time: (bars[0]?.time || new Date().getTime()) - index * this.minutesPerInterval[resolution],
time:
(bars[0]?.time || new Date().getTime()) - index * this.minutesPerInterval[resolution],
close: 0,
open: 0,
high: 0,

View File

@ -1,5 +1,5 @@
import { Suspense, useEffect, useMemo } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import AccountCreateFirst from 'components/Account/AccountCreateFirst'
import { CircularProgress } from 'components/CircularProgress'
@ -27,6 +27,7 @@ function FetchLoading() {
function Content() {
const address = useStore((s) => s.address)
const { address: urlAddress } = useParams()
const navigate = useNavigate()
const { pathname } = useLocation()
const { data: accountIds, isLoading: isLoadingAccounts } = useAccountIds(address || '')
@ -39,18 +40,26 @@ function Content() {
)
useEffect(() => {
const page = getPage(pathname)
if (page === 'portfolio' && urlAddress && urlAddress !== address) {
navigate(getRoute(page, urlAddress as string))
useStore.setState({ balances: walletBalances, focusComponent: null })
return
}
if (
accountIds &&
accountIds.length !== 0 &&
BN(baseBalance).isGreaterThanOrEqualTo(defaultFee.amount[0].amount)
) {
navigate(getRoute(getPage(pathname), address, accountIds[0]))
navigate(getRoute(page, address, accountIds[0]))
useStore.setState({ balances: walletBalances, focusComponent: null })
}
}, [accountIds, baseBalance, navigate, pathname, address, walletBalances])
}, [accountIds, baseBalance, navigate, pathname, address, walletBalances, urlAddress])
if (isLoadingAccounts || isLoadingBalances) return <FetchLoading />
if (BN(baseBalance).isLessThan(defaultFee.amount[0].amount)) return <WalletBridges />
if (accountIds.length === 0) return <AccountCreateFirst />
if (accountIds && accountIds.length === 0) return <AccountCreateFirst />
return null
}

View File

@ -2,9 +2,9 @@ import useSWR from 'swr'
import getAccountIds from 'api/wallets/getAccountIds'
export default function useAccountIds(address?: string) {
export default function useAccountIds(address?: string, suspense = true) {
return useSWR(`wallets/${address}/account-ids`, () => getAccountIds(address), {
suspense: true,
suspense: suspense,
fallback: [] as string[],
revalidateOnFocus: false,
})

View File

@ -4,6 +4,7 @@ import MigrationBanner from 'components/MigrationBanner'
import Balances from 'components/Portfolio/Account/Balances'
import BreadCrumbs from 'components/Portfolio/Account/BreadCrumbs'
import Summary from 'components/Portfolio/Account/Summary'
import ShareBar from 'components/ShareBar'
import useAccountId from 'hooks/useAccountId'
import { getRoute } from 'utils/route'
@ -23,6 +24,7 @@ export default function PortfolioAccountPage() {
<BreadCrumbs accountId={accountId} />
<Summary accountId={accountId} />
<Balances accountId={accountId} />
<ShareBar text={`Have a look at Credit Account ${accountId} on @mars_protocol!`} />
</div>
)
}

View File

@ -2,6 +2,7 @@ import MigrationBanner from 'components/MigrationBanner'
import AccountOverview from 'components/Portfolio/Overview'
import PortfolioSummary from 'components/Portfolio/Overview/Summary'
import PortfolioIntro from 'components/Portfolio/PortfolioIntro'
import ShareBar from 'components/ShareBar'
export default function PortfolioPage() {
return (
@ -10,6 +11,7 @@ export default function PortfolioPage() {
<PortfolioIntro />
<PortfolioSummary />
<AccountOverview />
<ShareBar text='Have a look at this @mars_protocol portfolio!' />
</div>
)
}

View File

@ -10,4 +10,5 @@ export enum DocURL {
TRADING_INTRO_URL = 'https://docs.marsprotocol.io/docs/learn/tutorials/trading/trading-intro',
V1_URL = 'https://v1.marsprotocol.io',
WALLET_INTRO_URL = 'https://docs.marsprotocol.io/docs/learn/tutorials/basics/connecting-your-wallet',
X_SHARE_URL = 'https://x.com/intent/tweet',
}