Skip to content

Commit

Permalink
feat: mf-4488 send token (#10220)
Browse files Browse the repository at this point in the history
* feat: mf-4488 send token

* refactor: reuse collectible list

* fixup! feat: mf-4488 send token
  • Loading branch information
UncleBill authored Jul 31, 2023
1 parent b55fed0 commit 7bf7075
Show file tree
Hide file tree
Showing 54 changed files with 1,205 additions and 2,238 deletions.
4 changes: 4 additions & 0 deletions packages/mask/shared-ui/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"approve": "Approve",
"address": "Address",
"amount": "Amount",
"max": "Max",
"available_balance": "Available Balance",
"available_amount": "{{- amount}} available",
"operation": "Operation",
"gas_limit": "Gas Limit",
"gas_price": "Gas Price",
Expand Down Expand Up @@ -54,6 +56,7 @@
"copy": "Copy",
"or": "Or",
"send": "Send",
"transfer_to": "To",
"congratulations": "Congratulations",
"token_standard": "Token Standard",
"persona_required": "Persona required.",
Expand Down Expand Up @@ -168,6 +171,7 @@
"failed": "Failed",
"buy_now": "Buy Now",
"no_enough_gas_fees": "No Enough Gas Fees",
"gas_fee": "Gas fee",
"open": "Open",
"settings": "Settings",
"do_not_see_your_nft": "Don't see your NFT?",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Icons } from '@masknet/icons'
import { MaskTextField, makeStyles } from '@masknet/theme'
import { openWindow } from '@masknet/shared-base-ui'
import { Box, Typography, useTheme } from '@mui/material'
import { Box, Typography, useTheme, type BoxProps } from '@mui/material'
import { memo, useCallback } from 'react'

import { useI18N } from '../../../../utils/index.js'
Expand All @@ -14,7 +14,6 @@ import type { NetworkPluginID } from '@masknet/shared-base'
const useStyles = makeStyles()((theme) => ({
input: {
flex: 1,
marginBottom: 8,
},
to: {
display: 'flex',
Expand All @@ -30,8 +29,6 @@ const useStyles = makeStyles()((theme) => ({
receiverPanel: {
display: 'flex',
alignItems: 'flex-start',
height: 88,
width: '100%',
},
inputText: {
fontSize: 10,
Expand Down Expand Up @@ -66,9 +63,9 @@ const useStyles = makeStyles()((theme) => ({
},
}))

const AddContactInputPanel = memo(function AddContactInputPanel() {
const AddContactInputPanel = memo(function AddContactInputPanel(props: BoxProps) {
const { t } = useI18N()
const { classes } = useStyles()
const { classes, cx } = useStyles()
const { chainId } = useChainContext<NetworkPluginID.PLUGIN_EVM>()
const { address, receiver, setReceiver, ensName, receiverValidationMessage, registeredAddress } =
ContactsContext.useContainer()
Expand All @@ -84,7 +81,7 @@ const AddContactInputPanel = memo(function AddContactInputPanel() {
}, [address, ensName])

return (
<Box padding={2} className={classes.receiverPanel}>
<Box padding={2} {...props} className={cx(classes.receiverPanel, props.className)}>
<div className={classes.to}>
<Typography className={classes.toText}>{t('popups_wallet_transfer_to')}</Typography>
</div>
Expand All @@ -107,7 +104,7 @@ const AddContactInputPanel = memo(function AddContactInputPanel() {
}}
/>
{receiverValidationMessage || registeredAddress ? (
<Typography className={receiverValidationMessage ? classes.validation : classes.receiver}>
<Typography className={receiverValidationMessage ? classes.validation : classes.receiver} mt={1}>
{receiverValidationMessage || registeredAddress}
{receiverValidationMessage ? null : (
<Icons.LinkOut
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export interface BottomDrawerProps extends PropsWithChildren {

export const BottomDrawer = memo<BottomDrawerProps>(function BottomDrawer({ open, onClose, children, title }) {
const { classes } = useStyles()
const handleClose = () => onClose?.()
return (
<Drawer anchor="bottom" onClose={onClose} open={open} classes={{ paper: classes.root }}>
<Drawer anchor="bottom" onClose={handleClose} open={open} classes={{ paper: classes.root }}>
<Box className={classes.header}>
<Typography className={classes.title}>{title}</Typography>
<Icons.Close size={24} onClick={onClose} />
<Icons.Close size={24} onClick={handleClose} />
</Box>
{children}
</Drawer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Icons } from '@masknet/icons'

interface GasSettingMenuProps {
gas: string
onChange: (config: GasConfig) => void
onChange?: (config: GasConfig) => void
}

export const GasSettingMenu = memo<GasSettingMenuProps>(function GasSettingMenu({ gas, onChange }) {
Expand All @@ -20,7 +20,7 @@ export const GasSettingMenu = memo<GasSettingMenuProps>(function GasSettingMenu(
const handleChange = useCallback(
(config: GasConfig, type?: GasOptionType) => {
if (type) setGasOptionType(type)
onChange(config)
onChange?.(config)
},
[onChange],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Icons } from '@masknet/icons'
import { ImageIcon, TokenIcon } from '@masknet/shared'
import { makeStyles } from '@masknet/theme'
import type { Web3Helper } from '@masknet/web3-helpers'
import { useWeb3Others } from '@masknet/web3-hooks-base'
import { formatBalance, type NetworkDescriptor } from '@masknet/web3-shared-base'
import type { ChainId, NetworkType } from '@masknet/web3-shared-evm'
import { Box, ListItem, ListItemIcon, ListItemText, Typography, type ListItemProps, Link } from '@mui/material'
import { memo, useEffect, useMemo, useRef } from 'react'

const useStyles = makeStyles()((theme) => {
return {
item: {
padding: theme.spacing(1, 1.5),
borderRadius: 8,
border: `1px solid ${theme.palette.maskColor.line}`,
marginBottom: theme.spacing(1),
},
selected: {
borderColor: theme.palette.maskColor.highlight,
},
tokenIcon: {
width: 36,
height: 36,
},
badgeIcon: {
position: 'absolute',
right: -6,
bottom: -4,
border: `1px solid ${theme.palette.common.white}`,
borderRadius: '50%',
},
text: {
fontSize: 16,
fontWeight: 700,
color: theme.palette.maskColor.main,
},
name: {
fontSize: 14,
color: theme.palette.maskColor.second,
display: 'flex',
alignItems: 'center',
},
balance: {
fontSize: 16,
fontWeight: 700,
color: theme.palette.maskColor.second,
},
link: {
color: theme.palette.maskColor.second,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: 4,
},
}
})

export interface TokenItemProps extends Omit<ListItemProps, 'onSelect'> {
asset: Web3Helper.FungibleAssetAll
network: NetworkDescriptor<ChainId, NetworkType> | undefined
selected?: boolean
onSelect?(asset: Web3Helper.FungibleAssetAll): void
}

export const TokenItem = memo(function TokenItem({
className,
asset,
network,
onSelect,
selected,
...rest
}: TokenItemProps) {
const { classes, cx } = useStyles()

const Others = useWeb3Others()
const explorerLink = useMemo(() => {
return Others.explorerResolver.fungibleTokenLink(asset.chainId, asset.address)
}, [asset.address, asset.chainId, Others.explorerResolver.fungibleTokenLink])

const liRef = useRef<HTMLLIElement>(null)
useEffect(() => {
if (!selected) return
liRef.current?.scrollIntoView()
}, [selected, liRef.current])

return (
<ListItem
secondaryAction={
<Typography className={classes.balance}>
{formatBalance(asset.balance, asset.decimals, 0, false, true, 5)}
</Typography>
}
className={cx(classes.item, className, selected ? classes.selected : null)}
onClick={() => onSelect?.(asset)}
ref={liRef}
{...rest}>
<ListItemIcon>
{/* TODO utility TokenIcon with badge */}
<Box position="relative">
<TokenIcon
className={classes.tokenIcon}
chainId={asset.chainId}
address={asset.address}
size={36}
/>
<ImageIcon className={classes.badgeIcon} size={16} icon={network?.icon} />
</Box>
</ListItemIcon>
<ListItemText
secondary={
<Typography className={classes.name}>
{asset.name}
<Link
onClick={(event) => event.stopPropagation()}
href={explorerLink}
className={classes.link}
target="_blank"
rel="noopener noreferrer">
<Icons.LinkOut size={18} />
</Link>
</Typography>
}>
<Typography className={classes.text}>{asset.symbol}</Typography>
</ListItemText>
</ListItem>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { SelectNetworkSidebar } from '@masknet/shared'
import { EMPTY_LIST, NetworkPluginID } from '@masknet/shared-base'
import type { Web3Helper } from '@masknet/web3-helpers'
import { useFungibleAssets, useNetworkDescriptors } from '@masknet/web3-hooks-base'
import type { ChainId } from '@masknet/web3-shared-evm'
import { Box, List, type BoxProps } from '@mui/material'
import { memo, useMemo, useState } from 'react'
import { TokenItem, type TokenItemProps } from './TokenItem.js'
import { makeStyles } from '@masknet/theme'
import { isSameAddress } from '@masknet/web3-shared-base'

const useStyles = makeStyles()((theme) => {
return {
picker: {
display: 'flex',
flexDirection: 'row',
overflow: 'auto',
},
list: {
overflow: 'auto',
width: '100%',
},
}
})

export interface TokenPickerProps extends Omit<BoxProps, 'onSelect'>, Pick<TokenItemProps, 'onSelect'> {
defaultChainId?: ChainId
chainId?: ChainId
address?: string
}

export const TokenPicker = memo(function TokenPicker({
defaultChainId,
chainId,
address,
className,
onSelect,
...rest
}: TokenPickerProps) {
const { classes, cx } = useStyles()
const { data: assets = EMPTY_LIST } = useFungibleAssets(NetworkPluginID.PLUGIN_EVM, undefined, {
chainId,
})
const [sidebarChainId, setSidebarChainId] = useState<Web3Helper.ChainIdAll | undefined>(defaultChainId)
const availableAssets = useMemo(() => {
if (!sidebarChainId) return assets
return assets.filter((x) => x.chainId === sidebarChainId)
}, [assets, sidebarChainId])

const allNetworks = useNetworkDescriptors(NetworkPluginID.PLUGIN_EVM)
const networks = useMemo(() => allNetworks.filter((x) => x.isMainnet), [allNetworks])

return (
<Box className={cx(classes.picker, className)} {...rest}>
<SelectNetworkSidebar
networks={networks}
pluginID={NetworkPluginID.PLUGIN_EVM}
chainId={sidebarChainId}
hiddenAllButton
onChainChange={setSidebarChainId}
/>
<List className={classes.list} data-hide-scrollbar>
{availableAssets.map((asset) => {
const network = allNetworks.find((x) => x.chainId === asset.chainId)
const selected = asset.chainId === chainId && isSameAddress(asset.address, address)

return (
<TokenItem
key={`${asset.chainId}.${asset.address}`}
asset={asset}
network={network}
selected={selected}
onSelect={onSelect}
/>
)
})}
</List>
</Box>
)
})
1 change: 1 addition & 0 deletions packages/mask/src/extension/popups/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './ActionModal/index.js'
export * from './NormalHeader/index.js'
export * from './BottomDrawer/index.js'
export * from './WalletBalance/index.js'
export * from './TokenPicker/index.js'
1 change: 1 addition & 0 deletions packages/mask/src/extension/popups/hook/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './useParamTab.js'
export * from './useTitle.js'
export * from './useTokenParams.js'
21 changes: 21 additions & 0 deletions packages/mask/src/extension/popups/hook/useParamTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'

export function useParamTab<T extends string>(defaultTab: T) {
const [params, setParams] = useSearchParams()
const tab = (params.get('tab') || defaultTab) as T
const handleTabChange = useCallback(
(_: unknown, tab: T) => {
setParams(
(params) => {
params.set('tab', tab)
return params.toString()
},
{ replace: true },
)
},
[setParams],
)

return [tab, handleTabChange] as const
}
18 changes: 16 additions & 2 deletions packages/mask/src/extension/popups/hook/useTokenParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@ import { getNativeTokenAddress, type ChainId } from '@masknet/web3-shared-evm'
import { useSearchParams } from 'react-router-dom'

export function useTokenParams() {
const [params] = useSearchParams()
const [params, setParams] = useSearchParams()
const defaultChainId = useChainId(NetworkPluginID.PLUGIN_EVM)
const rawChainId = params.get('chainId')
const chainId: ChainId = rawChainId ? Number.parseInt(rawChainId, 10) : defaultChainId
const rawAddress = params.get('address')
const address = rawAddress || getNativeTokenAddress(chainId)

return { chainId, address, rawChainId, rawAddress, params }
return { chainId, address, rawChainId, rawAddress, params, setParams }
}

/**
* No fallback for non-fungible token
*/
export function useNonFungibleTokenParams() {
const [params, setParams] = useSearchParams()
const defaultChainId = useChainId(NetworkPluginID.PLUGIN_EVM)
const rawChainId = params.get('nft:chainId')
const chainId: ChainId = rawChainId ? Number.parseInt(rawChainId, 10) : defaultChainId
const address = params.get('nft:address')
const tokenId = params.get('nft:tokenId')

return { chainId, address, tokenId, params, setParams }
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function AddContactDrawer({ onConfirm, address, name, setName, setAddress, ...re
value={name}
onChange={(ev) => setName(ev.target.value)}
error={nameAlreadyExist}
autoFocus
/>
<MaskTextField
spellCheck={false}
Expand Down
Loading

0 comments on commit 7bf7075

Please sign in to comment.