mirror of
https://github.com/status-im/status-web.git
synced 2026-08-31 06:11:10 +00:00
fix: wallet QA fixes (#1020)
* fix: send modal input * chore: update buy crypto drawer * fix: date format in collectibles detail * chore: regenerate token erc20 list with manually fixed BONK decimals * fix: currency amount format * fix: support ipfs icons and add fallback for missing icons * fix: set minimal transactino value * fix: improve balance validation in send assets modal * fix: build * Create blue-poems-mate.md * chore: address feedback * chore: update comment about wrapper BONK * chore: remove redundant lowercase * chore: remove log * fix: rename minimum transaction value constant and update styling
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@status-im/wallet": patch
|
||||
---
|
||||
|
||||
fix: wallet QA fixes
|
||||
@@ -14,6 +14,15 @@ const supportedNetworks = [
|
||||
8453, // Base
|
||||
]
|
||||
|
||||
/**
|
||||
* Manual overrides for tokens with incorrect decimals in upstream sources.
|
||||
* Key: checksummed contract address, Value: correct decimals
|
||||
* @see https://etherscan.io/token/{address} to verify on-chain decimals
|
||||
*/
|
||||
const decimalOverrides = {
|
||||
'0x1151CB3d861920e07a38e03eEAd12C32178567F6': 5, // Wrapped BONK (Ethereum ERC-20) - on-chain decimals on Ethereum is 5, not 18
|
||||
}
|
||||
|
||||
const standardTokenLists = [
|
||||
{ name: 'Uniswap', url: 'https://gateway.ipfs.io/ipns/tokens.uniswap.org' },
|
||||
// may include lowercased addresses (e.g. 0x744d70fdbe2ba4cf95131626614a1763df805b9e instead of 0x744d70FDBE2Ba4CF95131626614a1763DF805B9E)
|
||||
@@ -152,21 +161,26 @@ function generateTemplate(tokens) {
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
},
|
||||
tokens: tokens.map(token => ({
|
||||
chainId: token.chainId,
|
||||
address: token.address,
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
decimals: token.decimals,
|
||||
logoURI: token.logoURI,
|
||||
...(token.extensions &&
|
||||
token.extensions.bridgeInfo &&
|
||||
Object.keys(token.extensions.bridgeInfo).length > 0 && {
|
||||
extensions: {
|
||||
bridgeInfo: token.extensions.bridgeInfo,
|
||||
},
|
||||
}),
|
||||
})),
|
||||
tokens: tokens.map(token => {
|
||||
// Apply decimal overrides for tokens with incorrect upstream data
|
||||
const decimals = decimalOverrides[token.address] ?? token.decimals
|
||||
|
||||
return {
|
||||
chainId: token.chainId,
|
||||
address: token.address,
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
decimals,
|
||||
logoURI: token.logoURI,
|
||||
...(token.extensions &&
|
||||
token.extensions.bridgeInfo &&
|
||||
Object.keys(token.extensions.bridgeInfo).length > 0 && {
|
||||
extensions: {
|
||||
bridgeInfo: token.extensions.bridgeInfo,
|
||||
},
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
return JSON.stringify(tokenList, null, 2)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CurrencyAmount } from '../currency-amount'
|
||||
import { PercentageChange } from '../percentage-change'
|
||||
import * as Table from '../table'
|
||||
import { TokenAmount } from '../token-amount'
|
||||
import { TokenIcon } from '../token-icon'
|
||||
|
||||
import type { ApiOutput } from '../../data'
|
||||
|
||||
@@ -104,10 +105,11 @@ const AssetsList = (props: Props) => {
|
||||
>
|
||||
<Table.Cell size={36}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<img
|
||||
className="size-6 rounded-full bg-neutral-10"
|
||||
alt={asset.name}
|
||||
src={asset.icon}
|
||||
<TokenIcon
|
||||
icon={asset.icon}
|
||||
name={asset.name}
|
||||
symbol={asset.symbol}
|
||||
size="24"
|
||||
/>
|
||||
<span className="truncate">{asset.name}</span>
|
||||
</div>
|
||||
@@ -177,10 +179,11 @@ const AssetsList = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
className="size-8 rounded-full"
|
||||
alt={asset.name}
|
||||
src={asset.icon}
|
||||
<TokenIcon
|
||||
icon={asset.icon}
|
||||
name={asset.name}
|
||||
symbol={asset.symbol}
|
||||
size="32"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-col items-start text-left">
|
||||
<span className="w-full max-w-40 truncate text-15 font-600 sm:max-w-full">
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Avatar, useToast } from '@status-im/components'
|
||||
import { FeesIcon } from '@status-im/icons/12'
|
||||
import { ExternalIcon } from '@status-im/icons/20'
|
||||
|
||||
import { ERROR_MESSAGES } from '../../constants'
|
||||
import * as Drawer from '../drawer'
|
||||
import { Image, type ImageId } from '../image'
|
||||
import {
|
||||
getProviderUrl,
|
||||
ProviderCard,
|
||||
type ProviderInfo,
|
||||
PROVIDERS,
|
||||
} from '../provider-card'
|
||||
|
||||
import type { Account } from '../address'
|
||||
|
||||
@@ -25,26 +28,19 @@ export type Currency = {
|
||||
type NetworkOptions = Array<{
|
||||
id: 'ETHEREUM'
|
||||
name: string
|
||||
image: ImageId
|
||||
}>
|
||||
|
||||
const NETWORKS: NetworkOptions = [
|
||||
{
|
||||
id: 'ETHEREUM',
|
||||
name: 'Ethereum',
|
||||
image: 'Wallet/Icons/Logos/01:120:120',
|
||||
},
|
||||
]
|
||||
|
||||
export type BuyCryptoDrawerProps = {
|
||||
children: React.ReactElement
|
||||
account?: Account
|
||||
providers?: Array<{
|
||||
name: string
|
||||
description: string
|
||||
fee: string
|
||||
image: ImageId
|
||||
}>
|
||||
providers?: ProviderInfo[]
|
||||
onProviderSelect?: (
|
||||
provider: Provider,
|
||||
network: string,
|
||||
@@ -60,20 +56,7 @@ export const BuyCryptoDrawer = (props: Props) => {
|
||||
const {
|
||||
children,
|
||||
account,
|
||||
providers = [
|
||||
{
|
||||
name: 'mercuryo',
|
||||
// description: 'Buy crypto within 15 seconds',
|
||||
fee: '1% - 4.5%',
|
||||
image: 'Wallet/Icons/Logos/mercuryo:64:64',
|
||||
},
|
||||
{
|
||||
name: 'moonpay',
|
||||
// description: 'The new standard for fiat to crypto',
|
||||
fee: '1% - 4.5%',
|
||||
image: 'Wallet/Icons/Logos/moonpay:64:64',
|
||||
},
|
||||
],
|
||||
providers = PROVIDERS,
|
||||
onProviderSelect,
|
||||
onOpenTab,
|
||||
symbol,
|
||||
@@ -101,12 +84,11 @@ export const BuyCryptoDrawer = (props: Props) => {
|
||||
const data = await onProviderSelect(provider, network.id, currency.code)
|
||||
url = data.url
|
||||
} else {
|
||||
const providerUrls = {
|
||||
mercuryo: `https://exchange.mercuryo.io/?type=buy&network=${network.id}¤cy=${currency.code}&address=${account.address}&hide_address=false&fix_address=true&widget_id=6a7eb330-2b09-49b7-8fd3-1c77cfb6cd47`,
|
||||
moonpay: `https://buy.moonpay.com?apiKey=pk_live_YQC6CQPA5qqDu0unEwHJyAYQyeIqFGR`,
|
||||
}
|
||||
url = getProviderUrl(provider, account.address, {
|
||||
network: network.id,
|
||||
currency: currency.code,
|
||||
})
|
||||
|
||||
url = providerUrls[provider]
|
||||
if (!url) {
|
||||
toast.negative(ERROR_MESSAGES.PROVIDER_NOT_SUPPORTED)
|
||||
return
|
||||
@@ -165,43 +147,14 @@ export const BuyCryptoDrawer = (props: Props) => {
|
||||
</Drawer.Header>
|
||||
|
||||
<Drawer.Body className="relative flex flex-col overflow-clip">
|
||||
<div className="mt-2 flex flex-col gap-0.5 rounded-16 border border-neutral-10 bg-neutral-2.5 p-1">
|
||||
{providers.map(provider => {
|
||||
return (
|
||||
<button
|
||||
key={provider.name}
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-4 rounded-12 px-2 py-1 transition-colors hover:bg-neutral-5"
|
||||
onClick={() =>
|
||||
handleProviderSelect(provider.name as Provider)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image
|
||||
id={provider.image}
|
||||
alt={provider.name}
|
||||
className="size-8 rounded-full"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="text-15 font-600 capitalize">
|
||||
{provider.name}
|
||||
</div>
|
||||
{/* <div className="text-13 text-neutral-50">
|
||||
{provider.description}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1 text-13 font-500">
|
||||
<FeesIcon /> {provider.fee}
|
||||
</div>
|
||||
<ExternalIcon />
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<div className="mt-2 flex flex-col gap-3">
|
||||
{providers.map(provider => (
|
||||
<ProviderCard
|
||||
key={provider.name}
|
||||
{...provider}
|
||||
onClick={() => handleProviderSelect(provider.name as Provider)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Drawer.Body>
|
||||
</Drawer.Content>
|
||||
|
||||
@@ -21,6 +21,9 @@ const MIN_VALUE = 0.01
|
||||
export const CurrencyAmount = (props: Props) => {
|
||||
const { value, format = 'standard', className } = props
|
||||
|
||||
const factor = 10 ** 4
|
||||
const amount = Math.floor(value * factor) / factor
|
||||
|
||||
const formatter = useMemo(
|
||||
() =>
|
||||
match(format)
|
||||
@@ -62,9 +65,9 @@ export const CurrencyAmount = (props: Props) => {
|
||||
[format],
|
||||
)
|
||||
|
||||
if (value > 0 && value < MIN_VALUE && format !== 'precise') {
|
||||
if (amount > 0 && amount < MIN_VALUE && format !== 'precise') {
|
||||
return <div className={className}>{'< ' + formatter.format(MIN_VALUE)}</div>
|
||||
}
|
||||
|
||||
return <div className={className}>{formatter.format(value)}</div>
|
||||
return <div className={className}>{formatter.format(amount)}</div>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { ExternalIcon } from '@status-im/icons/12'
|
||||
import { cx } from 'class-variance-authority'
|
||||
|
||||
import { Image, type ImageId } from '../../image'
|
||||
import { getProviderUrl, ProviderCard, PROVIDERS } from '../../provider-card'
|
||||
|
||||
type Props = {
|
||||
address: string
|
||||
@@ -10,29 +7,6 @@ type Props = {
|
||||
const BuyTokens = (props: Props) => {
|
||||
const { address } = props
|
||||
|
||||
const PROVIDERS: ProviderProps[] = [
|
||||
{
|
||||
name: 'MoonPay',
|
||||
image: 'Wallet/Icons/Logos/moonpay-bigger:144:144',
|
||||
list: [
|
||||
'Pay with Credit/Debit Card, Bank Transfer, Apple/Google Pay, SEPA, +9 more',
|
||||
'Fees: from 1%',
|
||||
'Supported Countries: 180',
|
||||
],
|
||||
url: 'https://buy.moonpay.com/v2/buy?apiKey=pk_live_YQC6CQPA5qqDu0unEwHJyAYQyeIqFGR',
|
||||
},
|
||||
{
|
||||
name: 'Mercuryo',
|
||||
image: 'Wallet/Icons/Logos/mercuryo-bigger:144:144',
|
||||
list: [
|
||||
'Pay with Credit/Debit Card, Bank Transfer, Apple/Google Pay, SEPA, +10 more',
|
||||
'Fees: from 1%',
|
||||
'Supported Countries: 135+',
|
||||
],
|
||||
url: `https://exchange.mercuryo.io/?type=buy&network=ETHEREUM¤cy=ETH&address=${address}&hide_address=false&fix_address=true&widget_id=6a7eb330-2b09-49b7-8fd3-1c77cfb6cd47`,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start gap-4 rounded-16 bg-customisation-blue-50/5">
|
||||
<div className="flex flex-col gap-1 p-4 pb-1">
|
||||
@@ -45,55 +19,15 @@ const BuyTokens = (props: Props) => {
|
||||
</div>
|
||||
<div className="flex gap-3 p-3">
|
||||
{PROVIDERS.map(provider => (
|
||||
<Provider key={provider.name} {...provider} />
|
||||
<ProviderCard
|
||||
key={provider.name}
|
||||
{...provider}
|
||||
href={getProviderUrl(provider.name, address)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type ProviderProps = {
|
||||
name: string
|
||||
image: ImageId
|
||||
list: string[]
|
||||
url: string
|
||||
}
|
||||
|
||||
const Provider = (props: ProviderProps) => {
|
||||
const { name, image, list, url } = props
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-16 bg-white-100 p-4">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Image id={image} className="size-[72px]" />
|
||||
<h2 className="text-15 font-600">{name}</h2>
|
||||
</div>
|
||||
<ul className="mt-2 items-start text-13/[21px] font-400 text-neutral-50">
|
||||
{list.map(item => (
|
||||
<li key={item} className="ml-3 list-disc">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center justify-center">
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cx(
|
||||
'inline-flex h-6 shrink-0 cursor-pointer items-center justify-center gap-1',
|
||||
'rounded-8 border border-neutral-30 bg-transparent px-2 pr-[6px] text-13 font-500 text-neutral-100 outline-none transition-all',
|
||||
'active:border-neutral-20 active:bg-neutral-10 active:text-neutral-100 hover:border-neutral-40',
|
||||
'focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-80 focus-visible:ring-offset-2',
|
||||
'disabled:cursor-default disabled:opacity-[.3]',
|
||||
)}
|
||||
>
|
||||
Go to {name}
|
||||
<ExternalIcon className="shrink-0 text-neutral-50 [&>svg]:size-full" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { BuyTokens }
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ExternalIcon } from '@status-im/icons/12'
|
||||
import { cx } from 'class-variance-authority'
|
||||
|
||||
import { Image, type ImageId } from '../image'
|
||||
|
||||
export type ProviderInfo = {
|
||||
name: string
|
||||
image: ImageId
|
||||
list: string[]
|
||||
}
|
||||
|
||||
export const PROVIDERS: ProviderInfo[] = [
|
||||
{
|
||||
name: 'MoonPay',
|
||||
image: 'Wallet/Icons/Logos/moonpay-bigger:144:144',
|
||||
list: [
|
||||
'Pay with Credit/Debit Card, Bank Transfer, Apple/Google Pay, SEPA, +9 more',
|
||||
'Fees: from 1%',
|
||||
'Supported Countries: 180',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Mercuryo',
|
||||
image: 'Wallet/Icons/Logos/mercuryo-bigger:144:144',
|
||||
list: [
|
||||
'Pay with Credit/Debit Card, Bank Transfer, Apple/Google Pay, SEPA, +10 more',
|
||||
'Fees: from 1%',
|
||||
'Supported Countries: 135+',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const getProviderUrl = (
|
||||
provider: string,
|
||||
address?: string,
|
||||
options?: { network?: string; currency?: string },
|
||||
): string => {
|
||||
const { network = 'ETHEREUM', currency = 'ETH' } = options ?? {}
|
||||
|
||||
const urls: Record<string, string> = {
|
||||
moonpay:
|
||||
'https://buy.moonpay.com?apiKey=pk_live_YQC6CQPA5qqDu0unEwHJyAYQyeIqFGR',
|
||||
mercuryo: `https://exchange.mercuryo.io/?type=buy&network=${network}¤cy=${currency}&address=${address}&hide_address=false&fix_address=true&widget_id=6a7eb330-2b09-49b7-8fd3-1c77cfb6cd47`,
|
||||
}
|
||||
|
||||
return urls[provider.toLowerCase()] ?? ''
|
||||
}
|
||||
|
||||
type ProviderCardProps = ProviderInfo & {
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export const ProviderCard = (props: ProviderCardProps) => {
|
||||
const { name, image, list, href, onClick } = props
|
||||
|
||||
const cardClassName =
|
||||
'flex flex-col items-center gap-2 rounded-16 border border-neutral-10 bg-white-100 p-4'
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Image id={image} className="size-[72px]" />
|
||||
<h2 className="text-15 font-600">{name}</h2>
|
||||
<ul className="mt-2 w-full text-13/[21px] font-400 text-neutral-50">
|
||||
{list.map(item => (
|
||||
<li key={item} className="ml-3 list-disc text-left">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cx(
|
||||
cardClassName,
|
||||
'cursor-pointer transition-colors hover:bg-neutral-5',
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cardClassName}>
|
||||
{content}
|
||||
<div className="flex items-center justify-center">
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cx(
|
||||
'inline-flex h-6 shrink-0 cursor-pointer items-center justify-center gap-1',
|
||||
'rounded-8 border border-neutral-30 bg-transparent px-2 pr-[6px] text-13 font-500 text-neutral-100 outline-none transition-all',
|
||||
'active:border-neutral-20 active:bg-neutral-10 active:text-neutral-100 hover:border-neutral-40',
|
||||
'focus:outline-none focus-visible:ring-2 focus-visible:ring-neutral-80 focus-visible:ring-offset-2',
|
||||
)}
|
||||
>
|
||||
Go to {name}
|
||||
<ExternalIcon className="shrink-0 text-neutral-50" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { CurrencyAmount } from '../currency-amount'
|
||||
import { NetworkLogo } from '../network-logo'
|
||||
import { PasswordModal } from '../password-modal'
|
||||
import { TokenAmount } from '../token-amount'
|
||||
import { TokenIcon } from '../token-icon'
|
||||
|
||||
import type { NetworkType } from '../../data'
|
||||
import type { Account } from '../address'
|
||||
@@ -26,7 +27,7 @@ type Props = {
|
||||
}
|
||||
asset: {
|
||||
name: string
|
||||
icon: string
|
||||
icon?: string
|
||||
symbol: string
|
||||
totalBalance: number
|
||||
totalBalanceEur: number
|
||||
@@ -70,18 +71,18 @@ const createFormSchema = (
|
||||
.refine(
|
||||
val => {
|
||||
const amount = Number.parseFloat(val)
|
||||
if (!maxGasFeeEth) return amount > 0 && amount <= balance
|
||||
|
||||
const isETH = assetSymbol === 'ETH'
|
||||
const totalCost = isETH ? amount + maxGasFeeEth : maxGasFeeEth
|
||||
const availableBalance = balance
|
||||
|
||||
return amount > 0 && totalCost <= availableBalance
|
||||
return amount > 0 && amount <= balance
|
||||
},
|
||||
{ message: 'More than available balance' },
|
||||
)
|
||||
.refine(
|
||||
val => {
|
||||
if (!maxGasFeeEth || assetSymbol !== 'ETH') return true
|
||||
const amount = Number.parseFloat(val)
|
||||
return amount + maxGasFeeEth <= balance
|
||||
},
|
||||
{
|
||||
message: maxGasFeeEth
|
||||
? `Insufficient balance. Max gas fees: ${maxGasFeeEth.toFixed(6)} ETH`
|
||||
: 'More than available balance',
|
||||
message: `Insufficient balance. Max gas fees: ${(maxGasFeeEth ?? 0).toFixed(6)} ETH`,
|
||||
},
|
||||
),
|
||||
contractAddress: z
|
||||
@@ -150,6 +151,19 @@ const SendAssetsModal = (props: Props) => {
|
||||
const watchedTo = watch('to')
|
||||
const balanceEur = asset.totalBalanceEur
|
||||
|
||||
// Minimum transaction value in USD
|
||||
const MIN_TRANSACTION_AMOUNT_USD = 0.1
|
||||
|
||||
const amountFiatValue = useMemo(() => {
|
||||
if (!watchedAmount || balance === 0) return 0
|
||||
return Number.parseFloat(watchedAmount || '0') * (balanceEur / balance)
|
||||
}, [watchedAmount, balance, balanceEur])
|
||||
|
||||
const hasValueBelowMinimum =
|
||||
watchedAmount &&
|
||||
Number.parseFloat(watchedAmount) > 0 &&
|
||||
amountFiatValue < MIN_TRANSACTION_AMOUNT_USD
|
||||
|
||||
useEffect(() => {
|
||||
setValue('contractAddress', asset.contractAddress || undefined)
|
||||
}, [asset.contractAddress, setValue])
|
||||
@@ -284,7 +298,7 @@ const SendAssetsModal = (props: Props) => {
|
||||
data-customisation="blue"
|
||||
className="fixed left-0 top-[38px] flex size-full justify-center"
|
||||
>
|
||||
<div className="shadow opacity-100 fixed z-auto flex h-[calc(100vh-76px)] w-[calc(100%-23px)] max-w-[494px] flex-col gap-3 overflow-auto rounded-16 bg-white-100 transition data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in">
|
||||
<div className="shadow fixed z-auto flex h-[calc(100vh-76px)] w-[calc(100%-23px)] max-w-[494px] flex-col gap-3 overflow-auto rounded-16 bg-white-100 opacity-[100] transition data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<Dialog.Title className="text-27 font-semibold">
|
||||
Send assets
|
||||
@@ -305,7 +319,7 @@ const SendAssetsModal = (props: Props) => {
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex h-full flex-col place-content-between content-between justify-between space-y-6 px-4 pb-4"
|
||||
className="flex h-full flex-col place-content-between space-y-6 px-4 pb-4"
|
||||
>
|
||||
<div>
|
||||
<div className="mb-2 mt-4">
|
||||
@@ -356,7 +370,7 @@ const SendAssetsModal = (props: Props) => {
|
||||
step="any"
|
||||
placeholder="0"
|
||||
className={cx([
|
||||
'w-full px-4 py-3 text-27 font-medium',
|
||||
'w-full py-3 pl-4 pr-24 text-27 font-medium',
|
||||
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
|
||||
hasInsufficientBalance && 'text-danger-50',
|
||||
])}
|
||||
@@ -364,12 +378,13 @@ const SendAssetsModal = (props: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="absolute right-4 top-3 flex items-center gap-2">
|
||||
<div className="absolute right-4 top-3 flex items-center gap-2 bg-white-100">
|
||||
<div className="relative">
|
||||
<img
|
||||
className="size-8 rounded-full"
|
||||
alt={asset.name}
|
||||
src={asset.icon}
|
||||
<TokenIcon
|
||||
icon={asset.icon}
|
||||
name={asset.name}
|
||||
symbol={asset.symbol}
|
||||
size="32"
|
||||
/>
|
||||
<div className="absolute bottom-[-3px] right-[-3px] rounded-full border-2 border-white-100">
|
||||
<NetworkLogo name={asset.network} size={12} />
|
||||
@@ -408,12 +423,25 @@ const SendAssetsModal = (props: Props) => {
|
||||
</div>
|
||||
|
||||
{errors.amount && hasInsufficientBalance && (
|
||||
<div className="mt-2 flex items-center gap-1 text-13 text-danger-50">
|
||||
<div className="mt-2 flex items-start gap-1 text-13 text-danger-50">
|
||||
<AlertIcon className="size-4" />
|
||||
<p>{errors.amount.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasValueBelowMinimum && (
|
||||
<div className="mt-2 flex items-start gap-1 text-13 text-danger-50">
|
||||
<AlertIcon className="size-4" />
|
||||
<p>
|
||||
Value of the transaction must be at least{' '}
|
||||
<CurrencyAmount
|
||||
value={MIN_TRANSACTION_AMOUNT_USD}
|
||||
format="standard"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{watchedAmount && !hasInsufficientBalance && (
|
||||
<div className="mt-2 flex items-center gap-1 text-13 font-medium text-neutral-50">
|
||||
Remaining ~{' '}
|
||||
@@ -557,6 +585,7 @@ const SendAssetsModal = (props: Props) => {
|
||||
disabled={
|
||||
hasInsufficientBalance ||
|
||||
hasInsufficientEth ||
|
||||
hasValueBelowMinimum ||
|
||||
!watchedAmount ||
|
||||
!watchedTo ||
|
||||
!gasFees ||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cva, cx } from 'class-variance-authority'
|
||||
|
||||
type Props = {
|
||||
icon?: string
|
||||
name: string
|
||||
symbol: string
|
||||
size: '24' | '32'
|
||||
}
|
||||
|
||||
const tokenIconStyles = cva('rounded-full bg-neutral-10', {
|
||||
variants: {
|
||||
size: {
|
||||
'24': 'size-6 text-11',
|
||||
'32': 'size-8 text-13',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function resolveIconUrl(icon: string): string {
|
||||
if (icon.startsWith('ipfs://')) {
|
||||
const cid = icon.replace('ipfs://', '')
|
||||
return `https://ipfs.io/ipfs/${cid}`
|
||||
}
|
||||
return icon
|
||||
}
|
||||
|
||||
export function TokenIcon({ icon, name, symbol, size }: Props) {
|
||||
const initial = (symbol || name || '?').charAt(0).toUpperCase()
|
||||
if (icon) {
|
||||
const src = resolveIconUrl(icon)
|
||||
return <img src={src} alt={name} className={tokenIconStyles({ size })} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([
|
||||
'flex items-center justify-center bg-neutral-20',
|
||||
tokenIconStyles({ size }),
|
||||
])}
|
||||
>
|
||||
<span className="font-semibold text-neutral-40">{initial}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+14471
-8161
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,7 @@ const tokenMetadataSchema = z.object({
|
||||
|
||||
type Asset = {
|
||||
networks: NetworkType[]
|
||||
icon: string
|
||||
icon?: string
|
||||
name: string
|
||||
symbol: string
|
||||
price_eur: number
|
||||
@@ -105,7 +105,7 @@ export const DEFAULT_TOKEN_IDS: Record<string, string> = {
|
||||
|
||||
function buildTokenSummary(
|
||||
assets: Asset[],
|
||||
defaultIcon: string,
|
||||
defaultIcon: string | undefined,
|
||||
defaultName: string,
|
||||
defaultSymbol: string,
|
||||
) {
|
||||
@@ -114,7 +114,7 @@ function buildTokenSummary(
|
||||
|
||||
return {
|
||||
...summary,
|
||||
icon: defaultIcon,
|
||||
icon: defaultIcon ?? '',
|
||||
name: defaultName,
|
||||
symbol: defaultSymbol,
|
||||
about: firstAsset?.metadata.about ?? '',
|
||||
@@ -674,7 +674,7 @@ async function token({
|
||||
|
||||
if (
|
||||
!Number(result.tokenBalances[0].tokenBalance) &&
|
||||
!DEFAULT_TOKEN_SYMBOLS.includes(token.symbol)
|
||||
!(DEFAULT_TOKEN_SYMBOLS as readonly string[]).includes(token.symbol)
|
||||
) {
|
||||
throw new Error(`Balance not found for token ${token.symbol}`)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cache } from 'react'
|
||||
|
||||
import { format } from 'date-fns'
|
||||
import { z } from 'zod'
|
||||
|
||||
import {
|
||||
@@ -418,7 +419,22 @@ function map(
|
||||
traits: Array.isArray(attributes)
|
||||
? attributes.reduce(
|
||||
(acc, attribute) => {
|
||||
acc[attribute.trait_type] = attribute.value
|
||||
if (
|
||||
'display_type' in attribute &&
|
||||
attribute.display_type === 'date' &&
|
||||
typeof attribute.value === 'number'
|
||||
) {
|
||||
const timestamp =
|
||||
attribute.value < 10000000000
|
||||
? attribute.value * 1000
|
||||
: attribute.value
|
||||
acc[attribute.trait_type] = format(
|
||||
new Date(timestamp),
|
||||
'dd MMM yyyy',
|
||||
)
|
||||
} else {
|
||||
acc[attribute.trait_type] = attribute.value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, string | number | boolean>,
|
||||
|
||||
Reference in New Issue
Block a user