port wallet receive button (#706)

Co-authored-by: Felicio <felicio@users.noreply.github.com>
This commit is contained in:
marcelines
2025-06-26 14:58:18 +09:00
committed by GitHub
co-authored by Felicio
parent dec09cf966
commit ea4c1928d9
18 changed files with 209 additions and 123 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@status-im/wallet': patch
'portfolio': patch
'wallet': patch
---
port wallet receive button
@@ -1,7 +1,12 @@
'use client'
import { AssetsList } from '@status-im/wallet/components'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import {
useParams,
usePathname,
useRouter,
useSearchParams,
} from 'next/navigation'
import { useSearchAndSort } from '../../../../_hooks/use-search-and-sort'
@@ -17,6 +22,7 @@ const AssetsTable = (props: Props) => {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const { address } = useParams()
const { clearSearch } = useSearchAndSort()
@@ -24,7 +30,11 @@ const AssetsTable = (props: Props) => {
<AssetsList
assets={assets}
pathname={pathname}
onSelect={router.push}
onSelect={url => {
const ticker = url.split('/').pop()
if (!ticker) return
router.push(`/${address}/assets/${ticker}`)
}}
searchParams={searchParams}
clearSearch={clearSearch}
/>
@@ -105,9 +105,6 @@ const CollectiblesGrid = ({
searchParams={searchParams}
clearSearch={clearSearch}
hasNextPage={hasNextPage}
onSelect={() => {
// Handle select action if needed
}}
/>
)
}
@@ -1,13 +1,9 @@
'use client'
import { useState } from 'react'
import { Avatar, Button } from '@status-im/components'
import { CheckIcon, CopyIcon } from '@status-im/icons/20'
import { QRCodeSVG } from 'qrcode.react'
import { ReceiveCryptoDrawer as ReceiveCryptoDrawerBase } from '@status-im/wallet/components'
import { useCopyToClipboard } from '@status-im/wallet/hooks'
import { useCopyToClipboard } from '../_hooks/use-copy-to-clipboard'
import { useCurrentAccount } from '../_hooks/use-current-account'
import * as Drawer from './drawer'
import type React from 'react'
@@ -17,77 +13,11 @@ type Props = {
export const ReceiveCryptoDrawer = (props: Props) => {
const [, copy] = useCopyToClipboard()
const [open, setOpen] = useState(false)
const [success, setSuccess] = useState(false)
const { children } = props
const account = useCurrentAccount()
if (!account) {
return null
}
return (
<Drawer.Root modal open={open} onOpenChange={setOpen}>
{children && <Drawer.Trigger asChild>{children}</Drawer.Trigger>}
<Drawer.Content className="overflow-y-auto p-3">
<Drawer.Header className="sticky top-1 flex flex-col bg-white-60 px-1 pb-3 pt-1 backdrop-blur-[20px]">
<Drawer.Title>Receive</Drawer.Title>
<div
className="inline-flex h-6 items-center gap-1 self-start rounded-8 border bg-neutral-10 pl-px pr-2"
data-customisation={account.color}
>
<div className="rounded-6 bg-white-100">
<Avatar
type="account"
name={account.name}
emoji={account.emoji}
size="20"
bgOpacity="20"
/>
</div>
<span className="text-13 font-medium text-neutral-100">
{account.name}
</span>
</div>
</Drawer.Header>
<div
data-customisation={account.color}
className="grid gap-3 rounded-16 bg-customisation-50/5 p-3"
>
<div className="relative flex h-[444px] items-center justify-center overflow-hidden rounded-16 bg-white-100 shadow-3">
<QRCodeSVG value={account.address} size={408} className="" />
<div className="absolute left-1/2 top-1/2 z-20 -translate-x-1/2 -translate-y-1/2 transform rounded-16 bg-white-100">
<Avatar
type="account"
emoji={account.emoji}
size="80"
name={account.name}
bgOpacity="20"
/>
</div>
</div>
<div className="flex items-center justify-between font-mono text-15 text-neutral-50">
{account.address}{' '}
<Button
variant="outline"
onClick={() => {
copy(account.address)
setSuccess(true)
}}
size="32"
icon={
success ? (
<CheckIcon className="text-success-50" />
) : (
<CopyIcon className="text-neutral-50" />
)
}
aria-label="Copy code"
/>
</div>
</div>
</Drawer.Content>
</Drawer.Root>
)
return <ReceiveCryptoDrawerBase account={account} {...props} onCopy={copy} />
}
+5 -1
View File
@@ -12,7 +12,11 @@ const TabLink = ({ href, children, className }: Props) => {
const isActive = location.pathname.startsWith(href)
return (
<Link to={href} className={getTabLinkClassName(isActive, className)}>
<Link
to={href}
className={getTabLinkClassName(isActive, className)}
viewTransition
>
{children}
</Link>
)
+44
View File
@@ -0,0 +1,44 @@
'use client'
import { useEffect, useState } from 'react'
// @see https://tailwindcss.com/docs/screens
const screens = {
// We simulate the desktop first approach by using min-width 1px above the max-width of the previous breakpoint to match the design breakpoints
// Otherwise, we would have to use max-width approach and change the entire codebase styles
sm: '431px',
md: '641px',
'2md': '768px',
lg: '869px',
xl: '1024px',
'2xl': '1281px',
'3xl': '1441px',
// TODO to be defined by design for pro-users
'4xl': '1601px',
} as const
type Screen = keyof typeof screens
export function useMediaQuery(screen: Screen): boolean | null {
const [matches, setMatches] = useState<boolean | null>(null)
useEffect(() => {
const matchMedia = window.matchMedia(
`screen and (min-width: ${screens[screen]})`,
)
function handleChange() {
setMatches(matchMedia.matches)
}
handleChange()
matchMedia.addEventListener('change', handleChange)
return () => {
matchMedia.removeEventListener('change', handleChange)
}
}, [screen])
return matches
}
@@ -9,6 +9,7 @@ import {
import SplittedLayout from '@/components/splitted-layout'
import { useAssets } from '@/hooks/use-assets'
import { useMediaQuery } from '@/hooks/use-media-query'
import { useWallet } from '../../../providers/wallet-context'
import { Token } from './-components/token'
@@ -19,6 +20,7 @@ export const Route = createFileRoute('/portfolio/assets/$ticker')({
function Component() {
const { currentWallet, isLoading: isWalletLoading } = useWallet()
const isDesktop = useMediaQuery('xl')
const params = Route.useParams()
const ticker = params.ticker
@@ -50,6 +52,9 @@ function Component() {
router.navigate({
to: '/portfolio/assets/$ticker',
params: { ticker },
...(!isDesktop && {
viewTransition: true,
}),
})
}}
clearSearch={() => {
@@ -64,14 +69,14 @@ function Component() {
}
detail={
<Suspense fallback={<p>Loading token...</p>}>
<Token ticker={ticker} />
<Token ticker={ticker} address={address} />
</Suspense>
}
isLoading={isLoading}
/>
</div>
<div className="block 2xl:hidden">
<Token ticker={ticker} />
<Token ticker={ticker} address={address} />
</div>
</>
)
@@ -1,29 +1,47 @@
import { useEffect, useState } from 'react'
import { Button, Tooltip } from '@status-im/components'
import { BuyIcon, ReceiveBlurIcon } from '@status-im/icons/20'
import { ArrowLeftIcon, BuyIcon, ReceiveBlurIcon } from '@status-im/icons/20'
import {
Balance,
CurrencyAmount,
NetworkBreakdown,
ReceiveCryptoDrawer,
StickyHeaderContainer,
TokenAmount,
TokenLogo,
} from '@status-im/wallet/components'
import { useCopyToClipboard } from '@status-im/wallet/hooks'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { cx } from 'class-variance-authority'
import { renderMarkdown } from '@/lib/markdown'
import type { ApiOutput, NetworkType } from '@status-im/wallet/data'
import type { Account } from '@status-im/wallet/components'
import type { ApiOutput } from '@status-im/wallet/data'
type Props = {
address: string
ticker: string
}
const NETWORKS = [
'ethereum',
'optimism',
'arbitrum',
'base',
'polygon',
'bsc',
] as const
// todo?: Example address, replace with actual address when available
const ADDRESS = 'd8da6bf26964af9d7eed9e03e53415d37aa96045'
const Token = (props: Props) => {
const { ticker } = props
const { ticker, address } = props
const [markdownContent, setMarkdownContent] = useState<React.ReactNode>(null)
const [, copy] = useCopyToClipboard()
const token = useQuery<
ApiOutput['assets']['token'] | ApiOutput['assets']['nativeToken']
@@ -38,15 +56,8 @@ const Token = (props: Props) => {
'input',
JSON.stringify({
json: {
address: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045',
networks: [
'ethereum',
'optimism',
'arbitrum',
'base',
'polygon',
'bsc',
] as NetworkType[],
address,
networks: NETWORKS,
...(ticker.startsWith('0x')
? { contract: ticker }
: { symbol: ticker }),
@@ -98,6 +109,13 @@ const Token = (props: Props) => {
const uppercasedTicker = typedToken.summary.symbol
const icon = typedToken.summary.icon
const account: Account = {
address: ADDRESS,
name: 'Account 1',
emoji: '🍑',
color: 'magenta',
}
return (
<StickyHeaderContainer
className="-translate-x-0 !py-3 !pl-3 pr-[50px] 2xl:w-auto 2xl:!px-12 2xl:!py-4"
@@ -116,20 +134,34 @@ const Token = (props: Props) => {
Buy {typedToken.summary.name}
</span>
</Button>
<Button size="32" iconBefore={<ReceiveBlurIcon />}>
Receive
</Button>
<ReceiveCryptoDrawer account={account} onCopy={copy}>
<Button
size="32"
iconBefore={<ReceiveBlurIcon />}
variant="outline"
>
Receive
</Button>
</ReceiveCryptoDrawer>
</div>
}
>
<div className="-mt-8 grid gap-10 p-4 pt-0 2xl:mt-0 2xl:p-12 2xl:pt-0">
<Link
to="/portfolio/assets"
viewTransition
className="z-30 flex items-center gap-1 p-4 font-600 text-neutral-50 transition-colors hover:text-neutral-60 xl:hidden 2xl:mt-0 2xl:p-12 2xl:pt-0"
>
<ArrowLeftIcon />
Back
</Link>
<div className="grid gap-10 p-4 pt-0 2xl:mt-0 2xl:p-12 2xl:pt-0">
<div>
<TokenLogo
name={typedToken.summary.name}
ticker={uppercasedTicker}
icon={icon}
/>
<div className="my-6 2xl:mt-0">
<Balance variant="token" summary={typedToken.summary} />
</div>
@@ -139,13 +171,15 @@ const Token = (props: Props) => {
Buy {typedToken.summary.name}
</Button>
<Button
size="32"
variant="outline"
iconBefore={<ReceiveBlurIcon />}
>
Receive
</Button>
<ReceiveCryptoDrawer account={account} onCopy={copy}>
<Button
size="32"
variant="outline"
iconBefore={<ReceiveBlurIcon />}
>
Receive
</Button>
</ReceiveCryptoDrawer>
</div>
</div>
@@ -3,6 +3,7 @@ import { createFileRoute, useRouter } from '@tanstack/react-router'
import SplittedLayout from '@/components/splitted-layout'
import { useAssets } from '@/hooks/use-assets'
import { useMediaQuery } from '@/hooks/use-media-query'
import { usePinExtension } from '@/hooks/use-pin-extension'
import { useWallet } from '../../../providers/wallet-context'
@@ -22,6 +23,7 @@ function Component() {
address,
isWalletLoading,
})
const isDesktop = useMediaQuery('xl')
if (!currentWallet || !address) {
return <div>No wallet selected</div>
@@ -40,6 +42,9 @@ function Component() {
router.navigate({
to: '/portfolio/assets/$ticker',
params: { ticker },
...(!isDesktop && {
viewTransition: true,
}),
})
}}
clearSearch={() => {
@@ -23,7 +23,6 @@ export const Route = createFileRoute(
function Component() {
const { currentWallet, isLoading: isWalletLoading } = useWallet()
const router = useRouter()
const routerState = useRouterState()
const params = Route.useParams()
@@ -1,8 +1,14 @@
import { Button } from '@status-im/components'
import { ExternalIcon, OptionsIcon, SadIcon } from '@status-im/icons/20'
import {
ArrowLeftIcon,
ExternalIcon,
OptionsIcon,
SadIcon,
} from '@status-im/icons/20'
import { OpenseaIcon } from '@status-im/icons/social'
import { CurrencyAmount, NetworkLogo } from '@status-im/wallet/components'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import type { NetworkType } from '@status-im/wallet/data'
@@ -56,6 +62,14 @@ const Collectible = (props: Props) => {
return (
<div className="overflow-auto p-4 pr-3 2xl:p-12">
<Link
to="/portfolio/collectibles"
viewTransition
className="z-30 flex items-center gap-1 py-4 font-600 text-neutral-50 transition-colors hover:text-neutral-60 xl:hidden 2xl:mt-0 2xl:p-12 2xl:pt-0"
>
<ArrowLeftIcon />
Back
</Link>
<div className="mb-10 flex gap-4">
<div className="flex-1">
<div className="2xl:mb-6">
@@ -22,7 +22,12 @@ const LinkCollectible = (props: LinkProps) => {
}
return (
<LinkBase to={href} className={className} onClick={handleClick}>
<LinkBase
to={href}
className={className}
onClick={handleClick}
viewTransition
>
{children}
</LinkBase>
)
@@ -2,7 +2,7 @@ import {
CollectiblesGrid as CollectiblesList,
PinExtension,
} from '@status-im/wallet/components'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router'
import SplittedLayout from '@/components/splitted-layout'
import { useCollectibles } from '@/hooks/use-collectibles'
@@ -26,8 +26,6 @@ function Component() {
const { currentWallet, isLoading: isWalletLoading } = useWallet()
const { isPinExtension, handleClose } = usePinExtension()
const router = useRouter()
const searchParams = new URLSearchParams(window.location.search)
const search = searchParams.get('search') ?? undefined
@@ -66,13 +64,6 @@ function Component() {
console.log('Search cleared')
}}
hasNextPage={hasNextPage}
onSelect={url => {
const [network, contract, id] = url.split('/').slice(-3)
router.navigate({
to: '/portfolio/collectibles/$network/$contract/$id',
params: { network, contract, id },
})
}}
/>
}
isLoading={isLoading}
@@ -31,7 +31,6 @@ type Props = {
collectibles: Collectible[]
address: string
pathname: string
onSelect: (url: string, options?: { scroll?: boolean }) => void
search?: string
hasNextPage?: boolean
fetchNextPage: () => void
+1
View File
@@ -25,6 +25,7 @@ export { NetworkExplorerLogo } from './network-explorer-logo'
export { NetworkLogo } from './network-logo'
export { PercentageChange } from './percentage-change'
export { PinExtension } from './pin-extension'
export { ReceiveCryptoDrawer } from './receive-crypto-drawer'
export { RecoveryPhraseTextarea } from './recovery-phrase-textarea'
export { SettingsPopover } from './settings-popover'
export {
@@ -1,6 +1,6 @@
'use client'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Avatar, Button } from '@status-im/components'
import { CheckIcon, CopyIcon } from '@status-im/icons/20'
@@ -22,6 +22,16 @@ export const ReceiveCryptoDrawer = (props: Props) => {
const [success, setSuccess] = useState(false)
const { account, children, onCopy } = props
// Reset success state
useEffect(() => {
const timeout = setTimeout(() => {
if (success) {
setSuccess(false)
}
}, 2000)
return () => clearTimeout(timeout)
}, [success])
if (!account) {
return null
}
@@ -30,7 +40,7 @@ export const ReceiveCryptoDrawer = (props: Props) => {
<Drawer.Root modal open={open} onOpenChange={setOpen}>
{children && <Drawer.Trigger asChild>{children}</Drawer.Trigger>}
<Drawer.Content className="overflow-y-auto p-3">
<Drawer.Content className="!w-[calc(100%-24px)] !max-w-full overflow-y-auto p-3 md:!max-w-[494px]">
<Drawer.Header className="sticky top-1 flex flex-col bg-white-60 px-1 pb-3 pt-1 backdrop-blur-[20px]">
<Drawer.Title>Receive</Drawer.Title>
<div
@@ -55,8 +65,11 @@ export const ReceiveCryptoDrawer = (props: Props) => {
data-customisation={account.color}
className="grid gap-3 rounded-16 bg-customisation-50/5 p-3"
>
<div className="relative flex h-[444px] items-center justify-center overflow-hidden rounded-16 bg-white-100 shadow-3">
<QRCodeSVG value={account.address} size={408} className="" />
<div className="relative flex max-h-[444px] items-center justify-center overflow-hidden rounded-16 bg-white-100 shadow-3">
<QRCodeSVG
value={account.address}
style={{ width: '100%', height: '100%' }}
/>
<div className="absolute left-1/2 top-1/2 z-20 -translate-x-1/2 -translate-y-1/2 transform rounded-16 bg-white-100">
<Avatar
type="account"
+1
View File
@@ -1,2 +1,3 @@
export * from './use-copy-to-clipboard'
export * from './use-infinite-loading'
export * from './use-intersection-observer'
@@ -0,0 +1,27 @@
import { useState } from 'react'
type CopiedValue = string | null
type CopyFunction = (text: string) => Promise<boolean>
export function useCopyToClipboard(): [CopiedValue, CopyFunction] {
const [copiedText, setCopiedText] = useState<CopiedValue>(null)
const copy: CopyFunction = async text => {
if (!navigator?.clipboard) {
console.warn('Clipboard not supported')
return false
}
try {
await navigator.clipboard.writeText(text)
setCopiedText(text)
return true
} catch (error) {
console.warn('Copy failed', error)
setCopiedText(null)
return false
}
}
return [copiedText, copy]
}