mirror of
https://github.com/status-im/status-web.git
synced 2026-08-31 06:11:10 +00:00
port wallet receive button (#706)
Co-authored-by: Felicio <felicio@users.noreply.github.com>
This commit is contained in:
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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]
|
||||
}
|
||||
Reference in New Issue
Block a user