Skip to content

Commit

Permalink
Feat: copy addresses on click (#2838)
Browse files Browse the repository at this point in the history
* Feat: copy addresses on click

* Fix tests

* Disable copying in the Safe List and wallet menu
  • Loading branch information
katspaugh authored Nov 27, 2023
1 parent 63ec395 commit 8f09c2f
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 57 deletions.
3 changes: 0 additions & 3 deletions cypress/e2e/pages/owners.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as main from '../pages/main.page'
import * as createWallet from '../pages/create_wallet.pages'
import * as navigation from '../pages/navigation.page'

const copyToClipboardBtn = 'button[aria-label="Copy to clipboard"]'
const tooltipLabel = (label) => `span[aria-label="${label}"]`
const removeOwnerBtn = 'span[data-track="settings: Remove owner"] > span > button'
const replaceOwnerBtn = 'span[data-track="settings: Replace owner"] > span > button'
Expand Down Expand Up @@ -106,15 +105,13 @@ export function hoverOverDeleteOwnerBtn(index) {

export function openRemoveOwnerWindow(btn) {
cy.get(removeOwnerBtn).eq(btn).click({ force: true })
cy.get(copyToClipboardBtn).parent().eq(2).find('span').contains('0x').should('be.visible')
cy.get('div').contains(removeOwnerStr).should('exist')
}

export function openReplaceOwnerWindow() {
cy.get(replaceOwnerBtn).click({ force: true })
cy.get(newOwnerName).should('be.visible')
cy.get(newOwnerAddress).should('be.visible')
cy.get(copyToClipboardBtn).parent().eq(2).find('span').contains('0x').should('be.visible')
}
export function verifyTooltipLabel(label) {
cy.get(tooltipLabel(label)).should('be.visible')
Expand Down
7 changes: 4 additions & 3 deletions src/components/common/CopyAddressButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { type ReactElement } from 'react'

import type { ReactNode, ReactElement } from 'react'
import CopyButton from '../CopyButton'

const CopyAddressButton = ({
prefix,
address,
copyPrefix,
children,
}: {
prefix?: string
address: string
copyPrefix?: boolean
children?: ReactNode
}): ReactElement => {
const addressText = copyPrefix && prefix ? `${prefix}:${address}` : address

return <CopyButton text={addressText} />
return <CopyButton text={addressText}>{children}</CopyButton>
}

export default CopyAddressButton
49 changes: 10 additions & 39 deletions src/components/common/CopyButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ReactNode } from 'react'
import React, { type ReactElement, type SyntheticEvent, useCallback, useState } from 'react'
import React, { type ReactElement } from 'react'
import CopyIcon from '@/public/images/common/copy.svg'
import { IconButton, SvgIcon, Tooltip } from '@mui/material'
import { IconButton, SvgIcon } from '@mui/material'
import CopyTooltip from '../CopyTooltip'

const CopyButton = ({
text,
Expand All @@ -17,44 +18,14 @@ const CopyButton = ({
ariaLabel?: string
onCopy?: () => void
}): ReactElement => {
const [tooltipText, setTooltipText] = useState(initialToolTipText)
const [isCopyEnabled, setIsCopyEnabled] = useState(true)

const handleCopy = useCallback(
(e: SyntheticEvent) => {
e.preventDefault()
e.stopPropagation()
try {
navigator.clipboard.writeText(text).then(() => setTooltipText('Copied'))
onCopy?.()
} catch (err) {
setIsCopyEnabled(false)
setTooltipText('Copying is disabled in your browser')
}
},
[text, onCopy],
)

const handleMouseLeave = useCallback(() => {
setTimeout(() => {
if (isCopyEnabled) {
setTooltipText(initialToolTipText)
}
}, 500)
}, [initialToolTipText, isCopyEnabled])

return (
<Tooltip title={tooltipText} placement="top" onMouseLeave={handleMouseLeave}>
<IconButton
aria-label={initialToolTipText}
onClick={handleCopy}
size="small"
className={className}
disabled={!isCopyEnabled}
>
{children ?? <SvgIcon component={CopyIcon} inheritViewBox color="border" fontSize="small" />}
</IconButton>
</Tooltip>
<CopyTooltip text={text} onCopy={onCopy} initialToolTipText={initialToolTipText}>
{children ?? (
<IconButton aria-label={initialToolTipText} size="small" className={className}>
<SvgIcon component={CopyIcon} inheritViewBox color="border" fontSize="small" />
</IconButton>
)}
</CopyTooltip>
)
}

Expand Down
53 changes: 53 additions & 0 deletions src/components/common/CopyTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { ReactNode } from 'react'
import React, { type ReactElement, type SyntheticEvent, useCallback, useState } from 'react'
import { Tooltip } from '@mui/material'

const cursorPointer = { cursor: 'pointer' }

const CopyTooltip = ({
text,
children,
initialToolTipText = 'Copy to clipboard',
onCopy,
}: {
text: string
children?: ReactNode
initialToolTipText?: string
onCopy?: () => void
}): ReactElement => {
const [tooltipText, setTooltipText] = useState(initialToolTipText)
const [isCopyEnabled, setIsCopyEnabled] = useState(true)

const handleCopy = useCallback(
(e: SyntheticEvent) => {
e.preventDefault()
e.stopPropagation()
try {
navigator.clipboard.writeText(text).then(() => setTooltipText('Copied'))
onCopy?.()
} catch (err) {
setIsCopyEnabled(false)
setTooltipText('Copying is disabled in your browser')
}
},
[text, onCopy],
)

const handleMouseLeave = useCallback(() => {
setTimeout(() => {
if (isCopyEnabled) {
setTooltipText(initialToolTipText)
}
}, 500)
}, [initialToolTipText, isCopyEnabled])

return (
<Tooltip title={tooltipText} placement="top" onMouseLeave={handleMouseLeave}>
<span onClick={handleCopy} style={cursorPointer}>
{children}
</span>
</Tooltip>
)
}

export default CopyTooltip
24 changes: 18 additions & 6 deletions src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type EthHashInfoProps = {
showPrefix?: boolean
copyPrefix?: boolean
shortAddress?: boolean
copyAddress?: boolean
customAvatar?: string
hasExplorer?: boolean
avatarSize?: number
Expand All @@ -36,6 +37,7 @@ const SrcEthHashInfo = ({
copyPrefix,
showPrefix,
shortAddress = true,
copyAddress = true,
showAvatar = true,
avatarSize,
name,
Expand All @@ -47,8 +49,15 @@ const SrcEthHashInfo = ({
const shouldPrefix = isAddress(address)
const theme = useTheme()
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))

const identicon = <Identicon address={address} size={avatarSize} />
const shouldCopyPrefix = shouldPrefix && copyPrefix

const addressElement = (
<>
{showPrefix && shouldPrefix && prefix && <b>{prefix}:</b>}
<span>{shortAddress || isMobile ? shortenAddress(address) : address}</span>
</>
)

return (
<div className={css.container}>
Expand All @@ -74,13 +83,16 @@ const SrcEthHashInfo = ({

<div className={css.addressContainer}>
<Box fontWeight="inherit" fontSize="inherit">
{showPrefix && shouldPrefix && prefix && <b>{prefix}:</b>}
<span>{shortAddress || isMobile ? shortenAddress(address) : address}</span>
{copyAddress ? (
<CopyAddressButton prefix={prefix} address={address} copyPrefix={shouldCopyPrefix}>
{addressElement}
</CopyAddressButton>
) : (
addressElement
)}
</Box>

{showCopyButton && (
<CopyAddressButton prefix={prefix} address={address} copyPrefix={shouldPrefix && copyPrefix} />
)}
{showCopyButton && <CopyAddressButton prefix={prefix} address={address} copyPrefix={shouldCopyPrefix} />}

{hasExplorer && ExplorerButtonProps && (
<Box color="border.main">
Expand Down
9 changes: 8 additions & 1 deletion src/components/common/WalletOverview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ const WalletOverview = ({ wallet }: { wallet: ConnectedWallet }): ReactElement =
{wallet.ens ? (
<div>{wallet.ens}</div>
) : (
<EthHashInfo prefix={prefix || ''} address={wallet.address} showName={false} showAvatar avatarSize={12} />
<EthHashInfo
prefix={prefix || ''}
address={wallet.address}
showName={false}
showAvatar
avatarSize={12}
copyAddress={false}
/>
)}
</Typography>
</Box>
Expand Down
10 changes: 9 additions & 1 deletion src/components/sidebar/SafeListItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,15 @@ const SafeListItem = ({
}}
secondaryTypographyProps={{ component: 'div', color: 'primary' }}
primary={name || ''}
secondary={<EthHashInfo address={address} showAvatar={false} showName={false} prefix={shortName} />}
secondary={
<EthHashInfo
address={address}
showAvatar={false}
showName={false}
prefix={shortName}
copyAddress={false}
/>
}
/>
</ListItemButton>
</Link>
Expand Down
10 changes: 6 additions & 4 deletions src/components/sidebar/SidebarHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { selectSettings } from '@/store/settingsSlice'
import { useCurrentChain } from '@/hooks/useChains'
import { getBlockExplorerLink } from '@/utils/chains'
import EthHashInfo from '@/components/common/EthHashInfo'
import CopyButton from '@/components/common/CopyButton'
import QrCodeButton from '../QrCodeButton'
import Track from '@/components/common/Track'
import { OVERVIEW_EVENTS } from '@/services/analytics/events/overview'
Expand All @@ -29,6 +28,7 @@ import { useVisibleBalances } from '@/hooks/useVisibleBalances'
import EnvHintButton from '@/components/settings/EnvironmentVariables/EnvHintButton'
import useSafeAddress from '@/hooks/useSafeAddress'
import ExplorerButton from '@/components/common/ExplorerButton'
import CopyTooltip from '@/components/common/CopyTooltip'

const SafeHeader = (): ReactElement => {
const currency = useAppSelector(selectCurrency)
Expand Down Expand Up @@ -88,9 +88,11 @@ const SafeHeader = (): ReactElement => {
</Track>

<Track {...OVERVIEW_EVENTS.COPY_ADDRESS}>
<CopyButton text={addressCopyText} className={css.iconButton}>
<SvgIcon component={CopyIconBold} inheritViewBox color="primary" fontSize="small" />
</CopyButton>
<CopyTooltip text={addressCopyText}>
<IconButton className={css.iconButton}>
<SvgIcon component={CopyIconBold} inheritViewBox color="primary" fontSize="small" />
</IconButton>
</CopyTooltip>
</Track>

<Track {...OVERVIEW_EVENTS.OPEN_EXPLORER}>
Expand Down

0 comments on commit 8f09c2f

Please sign in to comment.