Skip to content

Commit

Permalink
Merge pull request #12 from eternalsafe/feat/clearer-magic-links
Browse files Browse the repository at this point in the history
remove default RPCs and improve magic link UX
  • Loading branch information
devanoneth authored Apr 30, 2024
2 parents 214e384 + 77652e3 commit cf9702b
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 54 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "eternal-safe",
"name": "eternalsafe-wallet",
"homepage": "https://github.com/eternalsafe/wallet",
"license": "GPL-3.0",
"version": "0.1.0",
"version": "0",
"type": "module",
"scripts": {
"dev": "next dev",
Expand Down
9 changes: 9 additions & 0 deletions src/components/common/OnboardingTooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export const OnboardingTooltip = ({
initiallyShown = true,
className,
placement,
open,
onOpen,
onClose,
}: {
children: ReactElement // NB: this has to be an actual HTML element, otherwise the Tooltip will not work
widgetLocalStorageId: string
Expand All @@ -26,6 +29,9 @@ export const OnboardingTooltip = ({
initiallyShown?: boolean
className?: string
placement?: TooltipProps['placement']
open?: boolean
onOpen?: () => void
onClose?: () => void
}): ReactElement => {
const [widgetHidden = !initiallyShown, setWidgetHidden] = useLocalStorage<boolean>(widgetLocalStorageId)
const isDarkMode = useDarkMode()
Expand All @@ -40,6 +46,9 @@ export const OnboardingTooltip = ({
title={postText}
disableInteractive
placement="top"
open={open}
onOpen={onOpen}
onClose={onClose}
>
{children}
</Tooltip>
Expand Down
122 changes: 110 additions & 12 deletions src/components/transactions/SignTxButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, type ReactElement, type SyntheticEvent } from 'react'
import { useCallback, useContext, useEffect, useState, type ReactElement, type SyntheticEvent } from 'react'
import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk'
import { Button, Tooltip } from '@mui/material'

Expand All @@ -7,11 +7,20 @@ import useWallet from '@/hooks/wallets/useWallet'
import useIsPending from '@/hooks/useIsPending'
import IconButton from '@mui/material/IconButton'
import CheckIcon from '@mui/icons-material/Check'
import ShareIcon from '@mui/icons-material/Share'
import CheckWallet from '@/components/common/CheckWallet'
import { useSafeSDK } from '@/hooks/coreSDK/safeCoreSDK'
import { getTxButtonTooltip } from '@/components/transactions/utils'
import { TxModalContext } from '@/components/tx-flow'
import { ConfirmTxFlow } from '@/components/tx-flow/flows'
import { useShareMagicLink } from '@/hooks/useMagicLink'
import { useSafeTransactionFromDetails } from '@/hooks/useConvertTx'
import { useRouter } from 'next/router'
import { AppRoutes } from '@/config/routes'
import { OnboardingTooltip } from '@/components/common/OnboardingTooltip'

const LS_MAGICLINK_ONBOARDING = 'magiclink_onboarding'
const INITIAL_TOOLTIP_TEXT = 'Copy Magic Link'

const SignTxButton = ({
txSummary,
Expand All @@ -28,29 +37,118 @@ const SignTxButton = ({
const isPending = useIsPending(txSummary.id)
const safeSDK = useSafeSDK()

const isDisabled = !isSignable || isPending || !safeSDK
const safeTx = useSafeTransactionFromDetails(txDetails)
const tx = useShareMagicLink(safeTx)

const router = useRouter()
const { safe } = router.query
const [link, setLink] = useState<string | undefined>(undefined)

const showMagicLink = !isSignable || isPending || !safeSDK

useEffect(() => {
if (tx && safe && showMagicLink) {
setLink(`${AppRoutes.transactions.tx}?safe=${safe}&tx=${tx}`)
} else {
setLink(undefined)
}
}, [tx, router.query, showMagicLink, safe])

const confirmTooltipTitle = getTxButtonTooltip('Confirm', { hasSafeSDK: !!safeSDK })

const [magicTooltipText, setMagicTooltipText] = useState(INITIAL_TOOLTIP_TEXT)
const [showMagicTooltip, setShowMagicTooltip] = useState(false)
const [isCopyEnabled, setIsCopyEnabled] = useState(true)

const onClick = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation()
e.preventDefault()

const tooltipTitle = getTxButtonTooltip('Confirm', { hasSafeSDK: !!safeSDK })
if (link) {
let timeout: NodeJS.Timeout | undefined

const onClick = (e: SyntheticEvent) => {
e.stopPropagation()
e.preventDefault()
setTxFlow(<ConfirmTxFlow txSummary={txSummary} txDetails={txDetails} />, undefined, false)
}
try {
const fullLink = location.origin + link

return (
const message = `Please sign this Eternal Safe transaction for the Safe: ${safe}.
Current confirmations: ${txDetails.detailedExecutionInfo.confirmations.length} of ${txDetails.detailedExecutionInfo.confirmationsRequired}.
${fullLink}
`

navigator.clipboard.writeText(message).then(() => setMagicTooltipText('Copied'))
setShowMagicTooltip(true)
timeout = setTimeout(() => {
if (isCopyEnabled) {
setShowMagicTooltip(false)
setMagicTooltipText(INITIAL_TOOLTIP_TEXT)
}
}, 1000)
} catch (err) {
setIsCopyEnabled(false)
setMagicTooltipText('Copying is disabled in your browser')
}
} else {
setTxFlow(<ConfirmTxFlow txSummary={txSummary} txDetails={txDetails} />, undefined, false)
}
},
[link, txDetails, setTxFlow, txSummary, isCopyEnabled, safe],
)

return showMagicLink ? (
<span>
{compact ? (
<Tooltip
title={magicTooltipText}
arrow
placement="top"
open={showMagicTooltip}
onOpen={() => setShowMagicTooltip(true)}
onClose={() => setShowMagicTooltip(false)}
>
<span>
<IconButton onClick={onClick} color="primary" disabled={!link} size="small">
<ShareIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
) : (
<OnboardingTooltip
widgetLocalStorageId={LS_MAGICLINK_ONBOARDING}
placement="top"
text={
<>
Eternal Safe relies on magic links to share transaction details.
<br />
These links are unique to each transaction and should be shared with other signers in order to collect
signatures and share full transaction details.
</>
}
postText={magicTooltipText}
open={showMagicTooltip}
onOpen={() => setShowMagicTooltip(true)}
onClose={() => setShowMagicTooltip(false)}
>
<Button sx={{ whiteSpace: 'nowrap' }} onClick={onClick} variant="contained" disabled={!link} size="stretched">
Magic Link
</Button>
</OnboardingTooltip>
)}
</span>
) : (
<CheckWallet>
{(isOk) =>
compact ? (
<Tooltip title={tooltipTitle} arrow placement="top">
<Tooltip title={confirmTooltipTitle} arrow placement="top">
<span>
<IconButton onClick={onClick} color="primary" disabled={!isOk || isDisabled} size="small">
<IconButton onClick={onClick} color="primary" disabled={!isOk} size="small">
<CheckIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
) : (
<Button onClick={onClick} variant="contained" disabled={!isOk || isDisabled} size="stretched">
<Button onClick={onClick} variant="contained" disabled={!isOk} size="stretched">
Confirm
</Button>
)
Expand Down
1 change: 0 additions & 1 deletion src/components/transactions/TxDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ const TxDetailsBlock = ({ txSummary, txDetails }: TxDetailsProps): ReactElement
{!isUnsigned && (
<div className={css.txSigners}>
<TxSigners txDetails={txDetails} txSummary={txSummary} />

{isQueue && (
<Box display="flex" alignItems="center" justifyContent="center" gap={1} mt={2}>
{awaitingExecution ? (
Expand Down
18 changes: 3 additions & 15 deletions src/components/transactions/TxShareLink/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { ReactElement, MouseEvent } from 'react'
import { IconButton, Link, SvgIcon } from '@mui/material'
import { IconButton, Link, SvgIcon, Tooltip } from '@mui/material'
import ShareIcon from '@/public/images/common/share.svg'
import { AppRoutes } from '@/config/routes'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import type { SafeTransaction } from '@safe-global/safe-core-sdk-types'
import { useShareMagicLink } from '@/hooks/useMagicLink'
import { OnboardingTooltip } from '@/components/common/OnboardingTooltip'

const LS_MAGICLINK_ONBOARDING = 'magiclink_onboarding'

Expand All @@ -33,18 +32,7 @@ const TxShareLink = ({ safeTx }: { safeTx: SafeTransaction }): ReactElement => {
}

return (
<OnboardingTooltip
widgetLocalStorageId={LS_MAGICLINK_ONBOARDING}
text={
<>
Eternal Safe relies on magic links to share transaction details.
<br />
These links are unique to each transaction and should be shared with other signers in order to collect
signatures and share full transaction details.
</>
}
postText="Copy magic link to clipboard"
>
<Tooltip title="Copy Magic Link" disableInteractive placement="top">
<IconButton
data-testid="share-btn"
component={Link}
Expand All @@ -55,7 +43,7 @@ const TxShareLink = ({ safeTx }: { safeTx: SafeTransaction }): ReactElement => {
>
<SvgIcon component={ShareIcon} inheritViewBox fontSize="small" color="border" />
</IconButton>
</OnboardingTooltip>
</Tooltip>
)
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/transactions/TxSigners/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ export const TxSigners = ({ txDetails, txSummary }: TxSignersProps): ReactElemen
const confirmationsCount = confirmations.length
const canExecute = wallet?.address ? isExecutable(txSummary, wallet.address, safe) : false
const confirmationsNeeded = confirmationsRequired - confirmations.length
const isConfirmed = confirmationsNeeded <= 0 || canExecute
const showConfirmationCount = confirmationsRequired > 0
const isConfirmed = confirmationsNeeded <= 0 || canExecute || executor
const showConfirmationCount = confirmationsRequired > 0 && !executor

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion src/components/transactions/TxSummary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const TxSummary = ({ item, txDetails, isGrouped }: TxSummaryProps): ReactElement
</Box>
)}

{wallet && isQueue && (
{isQueue && (
<Box
gridArea="actions"
display="flex"
Expand Down
18 changes: 6 additions & 12 deletions src/components/welcome/WelcomeLogin/LoadRPCUrl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { selectSettings, setRpc } from '@/store/settingsSlice'
import { useForm, FormProvider } from 'react-hook-form'
import useChainId from '@/hooks/useChainId'
import InfoIcon from '@/public/images/notifications/info.svg'
import { useCurrentChain } from '@/hooks/useChains'

export enum LoadRPCVariablesField {
rpc = 'rpc',
Expand All @@ -21,14 +20,13 @@ type LoadRPCUrlProps = {

const LoadRPCUrl = ({ hideRpcInput }: LoadRPCUrlProps) => {
const chainId = useChainId()
const chain = useCurrentChain()
const settings = useAppSelector(selectSettings)
const dispatch = useAppDispatch()

const formMethods = useForm<LoadRPCFormData>({
mode: 'onChange',
values: {
[LoadRPCVariablesField.rpc]: settings.env?.rpc[chainId] ?? chain?.publicRpcUri.value ?? '',
[LoadRPCVariablesField.rpc]: settings.env?.rpc[chainId] ?? '',
},
})

Expand Down Expand Up @@ -69,20 +67,16 @@ const LoadRPCUrl = ({ hideRpcInput }: LoadRPCUrlProps) => {
</Typography>

<TextField
{...register(LoadRPCVariablesField.rpc, { required: true })}
{...register(LoadRPCVariablesField.rpc, { required: true, validate: (value) => value.startsWith('https') })}
variant="outlined"
type="url"
InputProps={{
endAdornment: rpc ? null : (
endAdornment: !!settings.env?.rpc[chainId] ? (
<InputAdornment position="end">
<Tooltip title="Reset to default value">
<Tooltip title="Reset to saved value">
<IconButton
onClick={() =>
setValue(
LoadRPCVariablesField.rpc,
settings.env?.rpc[chainId] ?? chain?.publicRpcUri.value ?? '',
{ shouldValidate: true },
)
setValue(LoadRPCVariablesField.rpc, settings.env.rpc[chainId], { shouldValidate: true })
}
size="small"
color="primary"
Expand All @@ -91,7 +85,7 @@ const LoadRPCUrl = ({ hideRpcInput }: LoadRPCUrlProps) => {
</IconButton>
</Tooltip>
</InputAdornment>
),
) : null,
}}
fullWidth
required
Expand Down
10 changes: 3 additions & 7 deletions src/components/welcome/WelcomeLogin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ const WelcomeLogin = () => {

const [forceShowRpcInput, setForceShowRpcInput] = useState(false)

const providedPublic = chain?.publicRpcUri.value
? ` We have detected and prefilled a public RPC URL for ${chain.chainName}. More public URLs can be found on `
: ` If you don't have one, you can find a public RPC URL on `

const toggleShowRpcInput = () => {
setForceShowRpcInput(!forceShowRpcInput)
}
Expand Down Expand Up @@ -47,11 +43,11 @@ const WelcomeLogin = () => {
To get started you must provide a RPC URL for the {chain.chainName} network.
<br />
<br />
{providedPublic}
For best performance we recommend using a private RPC URL. Public URLs can be found on{' '}
<Link href={`${CHAINLIST_URL}chain/${chain.chainId}`} color="primary" target="_blank" rel="noreferrer">
Chainlist
</Link>
.
</Link>{' '}
however they may be rate limited or have other restrictions.
<br />
<br />
You can change this later in the settings.
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/useMagicLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,15 @@ export const useTransactionMagicLink = (): { tx: SafeTransaction | undefined; tx
return { tx, txKey }
}

export const useShareMagicLink = (tx: SafeTransaction): string | undefined => {
export const useShareMagicLink = (tx: SafeTransaction | undefined): string | undefined => {
const [encodedTx, setEncodedTx] = useState<string | undefined>()

useEffect(() => {
setEncodedTx(encodeTransactionMagicLink(tx))
if (tx) {
setEncodedTx(encodeTransactionMagicLink(tx))
} else {
setEncodedTx(undefined)
}
}, [tx])

return encodedTx
Expand Down

0 comments on commit cf9702b

Please sign in to comment.