From 1c922eca5db189ad49fb13dec853e5634497cb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 16 Jan 2025 14:58:39 +0000 Subject: [PATCH 01/21] Create new Safenet multi-transfer tx flow steps --- .../flows/SafenetTokenTransfers/index.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx new file mode 100644 index 0000000000..1977f237a0 --- /dev/null +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx @@ -0,0 +1,73 @@ +import { TokenAmountFields } from '@/components/common/TokenAmountInput' +import TxLayout from '@/components/tx-flow/common/TxLayout' +import AssetsIcon from '@/public/images/sidebar/assets.svg' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import useTxStepper from '../../useTxStepper' +import CreateTokenTransfers from './CreateTokenTransfers' +import ReviewTokenTransfers from './ReviewTokenTransfers' + +enum Fields { + recipient = 'recipient' +} + +export const TokenTransferFields = { ...Fields, ...TokenAmountFields } + +export type TokenTransferParams = { + [TokenTransferFields.recipient]: string + [TokenTransferFields.tokenAddress]: string + [TokenTransferFields.amount]: string +} + +enum TransfersFields { + recipients = 'recipients' +} + +export const TokenTransfersFields = { ...TransfersFields } + +export type TokenTransfersParams = { + recipients: TokenTransferParams[] +} + +type TokenTransferFlowProps = Partial & { + txNonce?: number +} + +const defaultParams: TokenTransfersParams = { + recipients: [{ + recipient: '', + tokenAddress: ZERO_ADDRESS, + amount: '' + }] +} + +const SafenetTokenTransfersFlow = ({ txNonce, ...params }: TokenTransferFlowProps) => { + + const { data, step, nextStep, prevStep } = useTxStepper({ + ...defaultParams, + ...params + }) + + const steps = [ + nextStep({ ...data, ...formData })} + />, + null} /> + ] + + return ( + + {steps} + + ) +} + +export default SafenetTokenTransfersFlow From 49929864907a2a76e8f393e8a1c83a7fcf80205f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 16 Jan 2025 15:52:40 +0000 Subject: [PATCH 02/21] Add create multi-recipient tx step --- .../common/TokenAmountInput/index.tsx | 33 ++-- .../CreateTokenTransfers.tsx | 142 ++++++++++++++++++ .../RecipientRow/index.tsx | 84 +++++++++++ .../flows/SafenetTokenTransfers/utils.ts | 20 +++ 4 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx create mode 100644 apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx create mode 100644 apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts diff --git a/apps/web/src/components/common/TokenAmountInput/index.tsx b/apps/web/src/components/common/TokenAmountInput/index.tsx index 19c0ebec8e..7e9916aa14 100644 --- a/apps/web/src/components/common/TokenAmountInput/index.tsx +++ b/apps/web/src/components/common/TokenAmountInput/index.tsx @@ -1,13 +1,13 @@ +import NumberField from '@/components/common/NumberField' +import { AutocompleteItem } from '@/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer' import { safeFormatUnits } from '@/utils/formatters' +import { validateDecimalLength, validateLimitedAmount } from '@/utils/validation' import { Button, Divider, FormControl, InputLabel, MenuItem, TextField } from '@mui/material' import { type SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' -import css from './styles.module.css' -import NumberField from '@/components/common/NumberField' -import { validateDecimalLength, validateLimitedAmount } from '@/utils/validation' -import { AutocompleteItem } from '@/components/tx-flow/flows/TokenTransfer/CreateTokenTransfer' -import { useFormContext } from 'react-hook-form' import classNames from 'classnames' import { useCallback } from 'react' +import { useFormContext } from 'react-hook-form' +import css from './styles.module.css' export enum TokenAmountFields { tokenAddress = 'tokenAddress', @@ -19,22 +19,29 @@ const TokenAmountInput = ({ selectedToken, maxAmount, validate, + groupName, }: { balances: SafeBalanceResponse['items'] selectedToken: SafeBalanceResponse['items'][number] | undefined maxAmount?: bigint validate?: (value: string) => string | undefined + groupName?: string }) => { + const fields = { + tokenAddress: groupName ? `${groupName}.${TokenAmountFields.tokenAddress}` : TokenAmountFields.tokenAddress, + amount: groupName ? `${groupName}.${TokenAmountFields.amount}` : TokenAmountFields.amount + } + const { formState: { errors }, register, resetField, watch, setValue, - } = useFormContext<{ [TokenAmountFields.tokenAddress]: string; [TokenAmountFields.amount]: string }>() + } = useFormContext() - const tokenAddress = watch(TokenAmountFields.tokenAddress) - const isAmountError = !!errors[TokenAmountFields.tokenAddress] || !!errors[TokenAmountFields.amount] + const tokenAddress = watch(fields.tokenAddress) + const isAmountError = !!errors[fields.tokenAddress] || !!errors[fields.amount] const validateAmount = useCallback( (value: string) => { @@ -47,7 +54,7 @@ const TokenAmountInput = ({ const onMaxAmountClick = useCallback(() => { if (!selectedToken || maxAmount === undefined) return - setValue(TokenAmountFields.amount, safeFormatUnits(maxAmount.toString(), selectedToken.tokenInfo.decimals), { + setValue(fields.amount, safeFormatUnits(maxAmount.toString(), selectedToken.tokenInfo.decimals), { shouldValidate: true, }) }, [maxAmount, selectedToken, setValue]) @@ -59,7 +66,7 @@ const TokenAmountInput = ({ fullWidth > - {errors[TokenAmountFields.tokenAddress]?.message || errors[TokenAmountFields.amount]?.message || 'Amount'} + {errors[fields.tokenAddress]?.message || errors[fields.amount]?.message || 'Amount' }
{ - resetField(TokenAmountFields.amount, { defaultValue: '' }) + resetField(fields.amount, { defaultValue: '' }) }, })} value={tokenAddress} diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx new file mode 100644 index 0000000000..47b2d6cb11 --- /dev/null +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -0,0 +1,142 @@ +import TokenIcon from '@/components/common/TokenIcon' +import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' +import TxCard from '@/components/tx-flow/common/TxCard' +import commonCss from '@/components/tx-flow/common/styles.module.css' +import AddIcon from '@/public/images/common/add.svg' +import { formatVisualAmount } from '@/utils/formatters' +import { Button, CardActions, Divider, Grid, SvgIcon, Typography } from '@mui/material' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { useContext, useEffect, type ReactElement } from 'react' +import { FormProvider, useFieldArray, useForm } from 'react-hook-form' +import { TokenTransfersFields, type TokenTransfersParams } from '.' +import RecipientRow from './RecipientRow' + +export const AutocompleteItem = (item: { tokenInfo: TokenInfo; balance: string }): ReactElement => ( + + + + + + {item.tokenInfo.name} + + + + {formatVisualAmount(item.balance, item.tokenInfo.decimals)} {item.tokenInfo.symbol} + + + +) + +export const CreateTokenTransfers = ({ + params, + onSubmit, + txNonce, +}: { + params: TokenTransfersParams + onSubmit: (data: TokenTransfersParams) => void + txNonce?: number +}): ReactElement => { + const { setNonce, setNonceNeeded } = useContext(SafeTxContext) + + useEffect(() => { + if (txNonce !== undefined) { + setNonce(txNonce) + } + }, [setNonce, txNonce]) + + const formMethods = useForm({ + defaultValues: { + ...params, + }, + mode: 'onChange', + delayError: 500, + }) + + const { + handleSubmit, + control + } = formMethods + + const { + fields: recipientFields, + append, + remove, + } = useFieldArray({ control, name: TokenTransfersFields.recipients }) + + const removeRecipient = (index: number): void => { + remove(index) + } + + useEffect(() => { + setNonceNeeded(true) + }, [setNonceNeeded]) + + return ( + + +
+ {recipientFields.map((field, i) => ( + 0} + groupName={TokenTransfersFields.recipients} + remove={removeRecipient} + /> + ))} + + + {recipientFields.length < 5 && ( + + )} + + {`${recipientFields.length}/5`} + + + + + + + + + +
+
+ ) +} + +export default CreateTokenTransfers diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx new file mode 100644 index 0000000000..48ace26142 --- /dev/null +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx @@ -0,0 +1,84 @@ +import AddressBookInput from '@/components/common/AddressBookInput' +import TokenAmountInput from '@/components/common/TokenAmountInput' +import DeleteIcon from '@/public/images/common/delete.svg' +import { Divider, FormControl, Grid, IconButton, SvgIcon, Typography } from '@mui/material' +import { useFormContext } from 'react-hook-form' +import { TokenTransfersFields, TokenTransfersParams } from '..' +import { useTokenAmount, useVisibleTokens } from '../utils' + +export const RecipientRow = ({ + index, + groupName, + removable = true, + remove +}: { + index: number + removable?: boolean + groupName: string + remove?: (index: number) => void +}) => { + const balancesItems = useVisibleTokens() + + const fieldName = `${groupName}.${index}` + const { + watch, + formState: { errors } + } = useFormContext() + + const recipient = watch(TokenTransfersFields.recipients) + + // TODO: Review tokens available for selection and max amount + const selectedToken = balancesItems.find((item) => item.tokenInfo.symbol === 'USDC') + const { maxAmount } = useTokenAmount(selectedToken) + + const isAddressValid = !!recipient && !errors[TokenTransfersFields.recipients]?.[index]?.recipient + + return ( + <> + {index > 0 && ( + + )} + + + {`Recipient ${index > 0 ? index + 1 : ''}`} + + {removable && ( + <> + remove?.(index)} aria-label="Remove recipient"> + + + + )} + + + + + + + + + + + ) +} + +export default RecipientRow diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts new file mode 100644 index 0000000000..865ccb04bb --- /dev/null +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts @@ -0,0 +1,20 @@ +import { useVisibleBalances } from '@/hooks/useVisibleBalances' +import { type SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' + +export const useTokenAmount = (selectedToken: SafeBalanceResponse['items'][0] | undefined) => { + const totalFiatAmount = BigInt(selectedToken?.fiatBalance || 0) * (10n ** BigInt(selectedToken?.tokenInfo.decimals || 1)) + const maxFiatAllowed = 100_000n + + // TODO: Fix maxFiatAllowed below to take fiatConversion into account. + const maxAmountAllowed = maxFiatAllowed * (10n ** BigInt(selectedToken?.tokenInfo.decimals || 1)) + + const maxAmount = totalFiatAmount >= maxAmountAllowed ? maxAmountAllowed : totalFiatAmount + + return { maxAmount } +} + +// TODO: Check visible tokens vs Safenet tokens +export const useVisibleTokens = () => { + const { balances } = useVisibleBalances() + return balances.items +} From 7944a7f4059e4994a463c88b62cd1f2f2932e34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 16 Jan 2025 15:54:11 +0000 Subject: [PATCH 03/21] Add safenet and size props to SendAmountBlock component --- .../flows/TokenTransfer/SendAmountBlock.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx b/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx index 6cc22ea22b..55058541d1 100644 --- a/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx +++ b/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx @@ -1,21 +1,25 @@ -import { type ReactNode } from 'react' -import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { Box, Typography } from '@mui/material' import TokenIcon from '@/components/common/TokenIcon' import FieldsGrid from '@/components/tx/FieldsGrid' import { formatVisualAmount } from '@/utils/formatters' +import { Box, Typography } from '@mui/material' +import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type ReactNode } from 'react' const SendAmountBlock = ({ amountInWei, tokenInfo, children, title = 'Send:', + safenet = false, + tokenSize, }: { /** Amount in WEI */ amountInWei: number | string tokenInfo: Omit & { logoUri?: string } children?: ReactNode - title?: string + title?: string, + safenet?: boolean, + tokenSize?: number }) => { return ( @@ -26,7 +30,7 @@ const SendAmountBlock = ({ gap: 1, }} > - + Date: Thu, 16 Jan 2025 15:54:30 +0000 Subject: [PATCH 04/21] Add review multi-recipient tx step --- .../ReviewTokenTransfers.tsx | 76 +++++++++++++++++++ .../flows/SafenetTokenTransfers/index.tsx | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx new file mode 100644 index 0000000000..46299da2c0 --- /dev/null +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx @@ -0,0 +1,76 @@ +import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' +import SendAmountBlock from '@/components/tx-flow/flows/TokenTransfer/SendAmountBlock' +import SendToBlock from '@/components/tx/SendToBlock' +import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' +import type { SubmitCallback } from '@/components/tx/SignOrExecuteForm/SignOrExecuteForm' +import useBalances from '@/hooks/useBalances' +import { createTokenTransferParams } from '@/services/tx/tokenTransferParams' +import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' +import { safeParseUnits } from '@/utils/formatters' +import { Divider, Grid } from '@mui/material' +import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import { useContext, useEffect } from 'react' +import type { TokenTransfersParams } from '.' + +const ReviewTokenTransfers = ({ + params, + onSubmit, + txNonce, +}: { + params: TokenTransfersParams + onSubmit: SubmitCallback + txNonce?: number +}) => { + const { setSafeTx, setSafeTxError, setNonce } = useContext(SafeTxContext) + const { balances } = useBalances() + + useEffect(() => { + if (txNonce !== undefined) { + setNonce(txNonce) + } + + const calls = params.recipients.map(recipient => { + const token = balances.items.find((item) => item.tokenInfo.address === recipient.tokenAddress) + + if (!token) return + + return createTokenTransferParams( + recipient.recipient, + recipient.amount, + token?.tokenInfo.decimals, + recipient.tokenAddress, + ) + }).filter((transfer): transfer is MetaTransactionData => !!transfer) + + createMultiSendCallOnlyTx(calls).then(setSafeTx).catch(setSafeTxError) + }, [params, txNonce, setNonce, setSafeTx, setSafeTxError]) + + return ( + + {params.recipients.map((recipient, index) => { + const token = balances.items.find((item) => item.tokenInfo.address === recipient.tokenAddress) + const amountInWei = safeParseUnits(recipient.amount, token?.tokenInfo.decimals)?.toString() || '0' + + return ( + <> + {index > 0 && } + + + {token && } + + + + ) + })} + + ) +} + +export default ReviewTokenTransfers diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx index 1977f237a0..25780c341a 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx @@ -1,8 +1,8 @@ import { TokenAmountFields } from '@/components/common/TokenAmountInput' import TxLayout from '@/components/tx-flow/common/TxLayout' +import useTxStepper from '@/components/tx-flow/useTxStepper' import AssetsIcon from '@/public/images/sidebar/assets.svg' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' -import useTxStepper from '../../useTxStepper' import CreateTokenTransfers from './CreateTokenTransfers' import ReviewTokenTransfers from './ReviewTokenTransfers' From 5a074bed8060acc10e7431de3a742e1bd37bb027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 16 Jan 2025 15:55:43 +0000 Subject: [PATCH 05/21] Update Safenet token icon --- apps/web/public/images/safenet-bright.svg | 49 ++++++------------- .../src/components/common/TokenIcon/index.tsx | 6 +-- .../common/TokenIcon/styles.module.css | 10 ++-- 3 files changed, 25 insertions(+), 40 deletions(-) diff --git a/apps/web/public/images/safenet-bright.svg b/apps/web/public/images/safenet-bright.svg index 3576e08580..cf754a1c1e 100644 --- a/apps/web/public/images/safenet-bright.svg +++ b/apps/web/public/images/safenet-bright.svg @@ -1,34 +1,15 @@ - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + diff --git a/apps/web/src/components/common/TokenIcon/index.tsx b/apps/web/src/components/common/TokenIcon/index.tsx index dde8fe7c05..641cbfc9ae 100644 --- a/apps/web/src/components/common/TokenIcon/index.tsx +++ b/apps/web/src/components/common/TokenIcon/index.tsx @@ -1,7 +1,7 @@ +import Image from 'next/image' import { useMemo, type ReactElement } from 'react' import ImageFallback from '../ImageFallback' import css from './styles.module.css' -import Image from 'next/image' const FALLBACK_ICON = '/images/common/token-placeholder.svg' const COINGECKO_THUMB = '/thumb/' @@ -25,7 +25,7 @@ const TokenIcon = ({ }, [logoUri]) return ( -
+
{safenet && (
- Safenet Logo + Safenet Logo
)}
diff --git a/apps/web/src/components/common/TokenIcon/styles.module.css b/apps/web/src/components/common/TokenIcon/styles.module.css index 8c0a4c0727..006d393478 100644 --- a/apps/web/src/components/common/TokenIcon/styles.module.css +++ b/apps/web/src/components/common/TokenIcon/styles.module.css @@ -2,6 +2,10 @@ position: relative; } +.additionalMargin { + margin-right: 6px; +} + .image { display: block; width: auto; @@ -15,9 +19,9 @@ .safenetContainer { position: absolute; - top: -40%; - right: -40%; - background-color: #1c1c1c; + top: -10px; + right: -10px; + background-color: var(--color-background-paper); border-radius: 50%; padding: 4px; display: flex; From 8e02507e0c42cf5f10fb664b8476e19cc12e385d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 16 Jan 2025 15:58:55 +0000 Subject: [PATCH 06/21] Add Safenet tx flow to New Transaction screen --- .../components/tx-flow/flows/NewTx/index.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/tx-flow/flows/NewTx/index.tsx b/apps/web/src/components/tx-flow/flows/NewTx/index.tsx index f6a79fafd8..b7758d6e51 100644 --- a/apps/web/src/components/tx-flow/flows/NewTx/index.tsx +++ b/apps/web/src/components/tx-flow/flows/NewTx/index.tsx @@ -1,20 +1,27 @@ -import { useCallback, useContext } from 'react' +import ChainIndicator from '@/components/common/ChainIndicator' +import { ProgressBar } from '@/components/common/ProgressBar' import { MakeASwapButton, SendTokensButton, TxBuilderButton } from '@/components/tx-flow/common/TxButton' +import { useTxBuilderApp } from '@/hooks/safe-apps/useTxBuilderApp' +import NewTxIcon from '@/public/images/transactions/new-tx.svg' import { Container, Grid, Paper, Typography } from '@mui/material' +import { useCallback, useContext } from 'react' import { TxModalContext } from '../../' import TokenTransferFlow from '../TokenTransfer' -import { useTxBuilderApp } from '@/hooks/safe-apps/useTxBuilderApp' -import { ProgressBar } from '@/components/common/ProgressBar' -import ChainIndicator from '@/components/common/ChainIndicator' -import NewTxIcon from '@/public/images/transactions/new-tx.svg' +import useIsSafenetEnabled from '@/hooks/useIsSafenetEnabled' +import SafenetTokenTransfersFlow from '../SafenetTokenTransfers' import css from './styles.module.css' const NewTxFlow = () => { const txBuilder = useTxBuilderApp() const { setTxFlow } = useContext(TxModalContext) + const isSafenetEnabled = useIsSafenetEnabled() const onTokensClick = useCallback(() => { + if (isSafenetEnabled) { + setTxFlow() + return + } setTxFlow() }, [setTxFlow]) From 44bb3c8c27ea71b7d28ccd4ad1a08bc008b85801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 16 Jan 2025 17:27:13 +0000 Subject: [PATCH 07/21] Add CSV Airdrop alert message --- .../CreateTokenTransfers.tsx | 50 +++++++++++++------ .../RecipientRow/index.tsx | 5 +- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx index 47b2d6cb11..38f14c2e8d 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -4,10 +4,10 @@ import TxCard from '@/components/tx-flow/common/TxCard' import commonCss from '@/components/tx-flow/common/styles.module.css' import AddIcon from '@/public/images/common/add.svg' import { formatVisualAmount } from '@/utils/formatters' -import { Button, CardActions, Divider, Grid, SvgIcon, Typography } from '@mui/material' +import { Alert, Button, CardActions, Divider, Grid, SvgIcon, Typography } from '@mui/material' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { useContext, useEffect, type ReactElement } from 'react' +import { useContext, useEffect, useState, type ReactElement } from 'react' import { FormProvider, useFieldArray, useForm } from 'react-hook-form' import { TokenTransfersFields, type TokenTransfersParams } from '.' import RecipientRow from './RecipientRow' @@ -48,6 +48,7 @@ export const CreateTokenTransfers = ({ onSubmit: (data: TokenTransfersParams) => void txNonce?: number }): ReactElement => { + const [maxRecipientsAlert, setMaxRecipientsAlert] = useState(false) const { setNonce, setNonceNeeded } = useContext(SafeTxContext) useEffect(() => { @@ -79,6 +80,17 @@ export const CreateTokenTransfers = ({ remove(index) } + const addRecipient = (): void => { + if (recipientFields.length === 1) { + setMaxRecipientsAlert(true) + } + append({ + recipient: '', + tokenAddress: ZERO_ADDRESS, + amount: '' + }) + } + useEffect(() => { setNonceNeeded(true) }, [setNonceNeeded]) @@ -88,13 +100,27 @@ export const CreateTokenTransfers = ({
{recipientFields.map((field, i) => ( - 0} - groupName={TokenTransfersFields.recipients} - remove={removeRecipient} - /> + <> + 0} + groupName={TokenTransfersFields.recipients} + remove={removeRecipient} + /> + {((i < recipientFields.length - 1) || (maxRecipientsAlert && i === 0)) && ( + + )} + {maxRecipientsAlert && i === 0 && ( + setMaxRecipientsAlert(false)} + > + If you want to add more than 5 recipients, use CSV Airdrop. + + )} + ))} append({ - recipient: '', - tokenAddress: ZERO_ADDRESS, - amount: '' - })} + onClick={addRecipient} startIcon={} size="large" > diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx index 48ace26142..a65e425b16 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx @@ -1,7 +1,7 @@ import AddressBookInput from '@/components/common/AddressBookInput' import TokenAmountInput from '@/components/common/TokenAmountInput' import DeleteIcon from '@/public/images/common/delete.svg' -import { Divider, FormControl, Grid, IconButton, SvgIcon, Typography } from '@mui/material' +import { FormControl, Grid, IconButton, SvgIcon, Typography } from '@mui/material' import { useFormContext } from 'react-hook-form' import { TokenTransfersFields, TokenTransfersParams } from '..' import { useTokenAmount, useVisibleTokens } from '../utils' @@ -35,9 +35,6 @@ export const RecipientRow = ({ return ( <> - {index > 0 && ( - - )} Date: Thu, 16 Jan 2025 20:38:16 +0000 Subject: [PATCH 08/21] Search Safe Apps by name --- .../common/BuyCryptoButton/index.tsx | 20 +++++------ .../GovernanceSection/GovernanceSection.tsx | 34 +++++++++---------- apps/web/src/config/constants.ts | 7 +++- .../ChooseRecoveryMethodModal.tsx | 26 +++++++------- .../hooks/__tests__/useRemoteSafeApps.test.ts | 8 ++--- .../src/hooks/safe-apps/useRemoteSafeApps.ts | 27 ++++++++++----- .../src/hooks/safe-apps/useTxBuilderApp.ts | 4 +-- apps/web/src/pages/balances/nfts.tsx | 10 +++--- 8 files changed, 76 insertions(+), 60 deletions(-) diff --git a/apps/web/src/components/common/BuyCryptoButton/index.tsx b/apps/web/src/components/common/BuyCryptoButton/index.tsx index 2694e78a14..7abb2ddaa8 100644 --- a/apps/web/src/components/common/BuyCryptoButton/index.tsx +++ b/apps/web/src/components/common/BuyCryptoButton/index.tsx @@ -1,21 +1,21 @@ -import { useTheme } from '@mui/material/styles' -import { usePathname, useSearchParams } from 'next/navigation' -import Link, { type LinkProps } from 'next/link' -import { Alert, Box, Button, ButtonBase, Typography, useMediaQuery } from '@mui/material' -import AddIcon from '@mui/icons-material/Add' import { SafeAppsTag } from '@/config/constants' import { AppRoutes } from '@/config/routes' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' +import RampLogo from '@/public/images/common/ramp_logo.svg' +import { OVERVIEW_EVENTS } from '@/services/analytics' import madProps from '@/utils/mad-props' -import { type ReactNode, useMemo } from 'react' +import AddIcon from '@mui/icons-material/Add' +import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded' +import { Alert, Box, Button, ButtonBase, Typography, useMediaQuery } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import Link, { type LinkProps } from 'next/link' +import { usePathname, useSearchParams } from 'next/navigation' +import { useMemo, type ReactNode } from 'react' import Track from '../Track' -import { OVERVIEW_EVENTS } from '@/services/analytics' -import RampLogo from '@/public/images/common/ramp_logo.svg' import css from './styles.module.css' -import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded' const useOnrampAppUrl = (): string | undefined => { - const [onrampApps] = useRemoteSafeApps(SafeAppsTag.ONRAMP) + const [onrampApps] = useRemoteSafeApps({ tag: SafeAppsTag.ONRAMP }) return onrampApps?.[0]?.url } diff --git a/apps/web/src/components/dashboard/GovernanceSection/GovernanceSection.tsx b/apps/web/src/components/dashboard/GovernanceSection/GovernanceSection.tsx index edb8b9f423..b4f7ff6f99 100644 --- a/apps/web/src/components/dashboard/GovernanceSection/GovernanceSection.tsx +++ b/apps/web/src/components/dashboard/GovernanceSection/GovernanceSection.tsx @@ -1,26 +1,26 @@ -import { useCallback, useEffect, useRef, useState } from 'react' -import { Typography, Card, Box, Link, SvgIcon } from '@mui/material' -import { WidgetBody } from '@/components/dashboard/styled' -import css from './styles.module.css' -import { useBrowserPermissions } from '@/hooks/safe-apps/permissions' -import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' -import { DISCORD_URL, SafeAppsTag } from '@/config/constants' -import { useDarkMode } from '@/hooks/useDarkMode' -import { OpenInNew } from '@mui/icons-material' -import NetworkError from '@/public/images/common/network-error.svg' -import useChainId from '@/hooks/useChainId' +import InfiniteScroll from '@/components/common/InfiniteScroll' import { getSafeTokenAddress } from '@/components/common/SafeTokenWidget' +import { WidgetBody } from '@/components/dashboard/styled' import SafeAppIframe from '@/components/safe-apps/AppFrame/SafeAppIframe' import type { UseAppCommunicatorHandlers } from '@/components/safe-apps/AppFrame/useAppCommunicator' import useAppCommunicator from '@/components/safe-apps/AppFrame/useAppCommunicator' -import { useCurrentChain } from '@/hooks/useChains' import useGetSafeInfo from '@/components/safe-apps/AppFrame/useGetSafeInfo' -import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' +import { getOrigin } from '@/components/safe-apps/utils' +import { DISCORD_URL, SafeAppsTag } from '@/config/constants' +import { useBrowserPermissions } from '@/hooks/safe-apps/permissions' +import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' +import useAsync from '@/hooks/useAsync' +import useChainId from '@/hooks/useChainId' +import { useCurrentChain } from '@/hooks/useChains' +import { useDarkMode } from '@/hooks/useDarkMode' import useSafeInfo from '@/hooks/useSafeInfo' +import NetworkError from '@/public/images/common/network-error.svg' import { fetchSafeAppFromManifest } from '@/services/safe-apps/manifest' -import useAsync from '@/hooks/useAsync' -import { getOrigin } from '@/components/safe-apps/utils' -import InfiniteScroll from '@/components/common/InfiniteScroll' +import { OpenInNew } from '@mui/icons-material' +import { Box, Card, Link, SvgIcon, Typography } from '@mui/material' +import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' +import { useCallback, useEffect, useRef, useState } from 'react' +import css from './styles.module.css' // A fallback component when the Safe App fails to load const WidgetLoadErrorFallback = () => ( @@ -100,7 +100,7 @@ const MiniAppFrame = ({ app, title }: { app: SafeAppData; title: string }) => { // Entire section for the governance widgets const GovernanceSection = () => { - const [matchingApps, errorFetchingGovernanceSafeApp] = useRemoteSafeApps(SafeAppsTag.SAFE_GOVERNANCE_APP) + const [matchingApps, errorFetchingGovernanceSafeApp] = useRemoteSafeApps({ tag: SafeAppsTag.SAFE_GOVERNANCE_APP }) const governanceApp = matchingApps?.[0] const fetchingSafeGovernanceApp = !governanceApp && !errorFetchingGovernanceSafeApp const { safeLoading } = useSafeInfo() diff --git a/apps/web/src/config/constants.ts b/apps/web/src/config/constants.ts index cd9a898333..8ff2838da2 100644 --- a/apps/web/src/config/constants.ts +++ b/apps/web/src/config/constants.ts @@ -61,7 +61,12 @@ export enum SafeAppsTag { TX_BUILDER = 'transaction-builder', SAFE_GOVERNANCE_APP = 'safe-governance-app', ONRAMP = 'onramp', - RECOVERY_SYGNUM = 'recovery-sygnum', + RECOVERY_SYGNUM = 'recovery-sygnum' +} + +// Safe Apps names +export enum SafeAppsName { + CSV = 'CSV Airdrop' } // Help Center diff --git a/apps/web/src/features/recovery/components/RecoverySettings/ChooseRecoveryMethodModal.tsx b/apps/web/src/features/recovery/components/RecoverySettings/ChooseRecoveryMethodModal.tsx index 7f6b1ec2ca..299cbe620a 100644 --- a/apps/web/src/features/recovery/components/RecoverySettings/ChooseRecoveryMethodModal.tsx +++ b/apps/web/src/features/recovery/components/RecoverySettings/ChooseRecoveryMethodModal.tsx @@ -1,10 +1,8 @@ import Track from '@/components/common/Track' -import { RECOVERY_FEEDBACK_FORM, HelpCenterArticle, SafeAppsTag } from '@/config/constants' +import { HelpCenterArticle, RECOVERY_FEEDBACK_FORM, SafeAppsTag } from '@/config/constants' import { trackEvent } from '@/services/analytics' import { RECOVERY_EVENTS } from '@/services/analytics/events/recovery' -import { type ChangeEvent, type ReactElement, useContext, useState, useCallback } from 'react' -import { Controller, useForm } from 'react-hook-form' -import Link from 'next/link' +import CloseIcon from '@mui/icons-material/Close' import { Box, Button, @@ -20,21 +18,23 @@ import { RadioGroup, Typography, } from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' +import Link from 'next/link' +import { useCallback, useContext, useState, type ChangeEvent, type ReactElement } from 'react' +import { Controller, useForm } from 'react-hook-form' -import { UpsertRecoveryFlow } from '@/components/tx-flow/flows' import ExternalLink from '@/components/common/ExternalLink' +import TxStatusChip from '@/components/transactions/TxStatusChip' +import { TxModalContext } from '@/components/tx-flow' +import { UpsertRecoveryFlow } from '@/components/tx-flow/flows' +import { AppRoutes } from '@/config/routes' +import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' +import CheckIcon from '@/public/images/common/check.svg' import RecoveryCustomIcon from '@/public/images/common/recovery_custom.svg' import RecoverySygnumIcon from '@/public/images/common/recovery_sygnum.svg' import RecoveryZkEmailIcon from '@/public/images/common/zkemail-logo.svg' -import { TxModalContext } from '@/components/tx-flow' -import css from './styles.module.css' -import CheckIcon from '@/public/images/common/check.svg' -import { AppRoutes } from '@/config/routes' import { useSearchParams } from 'next/navigation' -import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' -import TxStatusChip from '@/components/transactions/TxStatusChip' import { ZkEmailFakeDoorModal } from './ZkEmailFakeDoorModal' +import css from './styles.module.css' enum RecoveryMethod { SelfCustody = 'SelfCustody', @@ -54,7 +54,7 @@ export function ChooseRecoveryMethodModal({ open, onClose }: { open: boolean; on const { setTxFlow } = useContext(TxModalContext) const [openZkEmailModal, setOpenZkEmailModal] = useState(false) const querySafe = useSearchParams().get('safe') - const [matchingApps] = useRemoteSafeApps(SafeAppsTag.RECOVERY_SYGNUM) + const [matchingApps] = useRemoteSafeApps({ tag: SafeAppsTag.RECOVERY_SYGNUM }) const hasSygnumApp = Boolean(matchingApps?.length) const methods = useForm({ diff --git a/apps/web/src/hooks/__tests__/useRemoteSafeApps.test.ts b/apps/web/src/hooks/__tests__/useRemoteSafeApps.test.ts index da77750c1f..edc658c847 100644 --- a/apps/web/src/hooks/__tests__/useRemoteSafeApps.test.ts +++ b/apps/web/src/hooks/__tests__/useRemoteSafeApps.test.ts @@ -1,9 +1,9 @@ -import { act, renderHook } from '@testing-library/react' import * as gateway from '@safe-global/safe-gateway-typescript-sdk' +import { act, renderHook } from '@testing-library/react' -import * as useChainIdHook from '@/hooks/useChainId' -import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' import type { SafeAppsTag } from '@/config/constants' +import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' +import * as useChainIdHook from '@/hooks/useChainId' jest.mock('@safe-global/safe-gateway-typescript-sdk') @@ -47,7 +47,7 @@ describe('useRemoteSafeApps', () => { ]) }) it('should alphabetically return the remote safe apps filtered by tag', async () => { - const { result } = renderHook(() => useRemoteSafeApps('test' as SafeAppsTag)) + const { result } = renderHook(() => useRemoteSafeApps({ tag: 'test' as SafeAppsTag })) var [data, error, loading] = result.current diff --git a/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts b/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts index 0cd9bd4d2a..a91856809d 100644 --- a/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts +++ b/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts @@ -1,11 +1,11 @@ -import { useEffect, useMemo } from 'react' +import { SafeAppsName, SafeAppsTag } from '@/config/constants' +import useChainId from '@/hooks/useChainId' +import { Errors, logError } from '@/services/exceptions' import type { SafeAppsResponse } from '@safe-global/safe-gateway-typescript-sdk' import { getSafeApps } from '@safe-global/safe-gateway-typescript-sdk' -import { Errors, logError } from '@/services/exceptions' -import useChainId from '@/hooks/useChainId' +import { useEffect, useMemo } from 'react' import type { AsyncResult } from '../useAsync' import useAsync from '../useAsync' -import type { SafeAppsTag } from '@/config/constants' // To avoid multiple simultaneous requests (e.g. the Dashboard and the SAFE header widget), // cache the request promise for 100ms @@ -25,7 +25,12 @@ const cachedGetSafeApps = (chainId: string): ReturnType | un return cache[chainId] } -const useRemoteSafeApps = (tag?: SafeAppsTag): AsyncResult => { +type UseRemoteSafeAppsProps = + | { tag: SafeAppsTag; name?: never } + | { name: SafeAppsName; tag?: never } + | { name?: never; tag?: never } + +const useRemoteSafeApps = ({ tag, name }: UseRemoteSafeAppsProps = {}): AsyncResult => { const chainId = useChainId() const [remoteApps, error, loading] = useAsync(() => { @@ -40,9 +45,15 @@ const useRemoteSafeApps = (tag?: SafeAppsTag): AsyncResult => }, [error]) const apps = useMemo(() => { - if (!remoteApps || !tag) return remoteApps - return remoteApps.filter((app) => app.tags.includes(tag)) - }, [remoteApps, tag]) + if (!remoteApps) return remoteApps + if (tag) { + return remoteApps.filter((app) => app.tags.includes(tag)) + } + if (name) { + return remoteApps.filter((app) => app.name.includes(name)) + } + return remoteApps + }, [remoteApps, tag, name]) const sortedApps = useMemo(() => { return apps?.sort((a, b) => a.name.localeCompare(b.name)) diff --git a/apps/web/src/hooks/safe-apps/useTxBuilderApp.ts b/apps/web/src/hooks/safe-apps/useTxBuilderApp.ts index 84b54331fa..753698e9d6 100644 --- a/apps/web/src/hooks/safe-apps/useTxBuilderApp.ts +++ b/apps/web/src/hooks/safe-apps/useTxBuilderApp.ts @@ -1,5 +1,5 @@ -import { useRouter } from 'next/router' import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' +import { useRouter } from 'next/router' import type { UrlObject } from 'url' import { SafeAppsTag } from '@/config/constants' @@ -7,7 +7,7 @@ import { AppRoutes } from '@/config/routes' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' export const useTxBuilderApp = (): { app?: SafeAppData; link: UrlObject } | undefined => { - const [matchingApps] = useRemoteSafeApps(SafeAppsTag.TX_BUILDER) + const [matchingApps] = useRemoteSafeApps({ tag: SafeAppsTag.TX_BUILDER }) const router = useRouter() const app = matchingApps?.[0] diff --git a/apps/web/src/pages/balances/nfts.tsx b/apps/web/src/pages/balances/nfts.tsx index e8e2fa77ce..dc7f101dcd 100644 --- a/apps/web/src/pages/balances/nfts.tsx +++ b/apps/web/src/pages/balances/nfts.tsx @@ -1,16 +1,16 @@ -import { type ReactElement, memo } from 'react' -import type { NextPage } from 'next' -import Head from 'next/head' -import { Grid, Skeleton, Typography } from '@mui/material' import AssetsHeader from '@/components/balances/AssetsHeader' import NftCollections from '@/components/nfts/NftCollections' import SafeAppCard from '@/components/safe-apps/SafeAppCard' import { BRAND_NAME, SafeAppsTag } from '@/config/constants' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' +import { Grid, Skeleton, Typography } from '@mui/material' +import type { NextPage } from 'next' +import Head from 'next/head' +import { memo, type ReactElement } from 'react' // `React.memo` requires a `displayName` const NftApps = memo(function NftApps(): ReactElement | null { - const [nftApps] = useRemoteSafeApps(SafeAppsTag.NFT) + const [nftApps] = useRemoteSafeApps({ tag: SafeAppsTag.NFT }) if (nftApps?.length === 0) { return null From e6db4be70f9f6e969b9268f924397f383e94ff18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Thu, 16 Jan 2025 20:39:20 +0000 Subject: [PATCH 09/21] Add CSV Airdrop modal --- .../images/apps/csv-airdrop-app-logo.svg | 15 +++++ .../CSVAirdropAppModal/index.tsx | 50 +++++++++++++++++ .../CreateTokenTransfers.tsx | 56 ++++++++++++++----- 3 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 apps/web/public/images/apps/csv-airdrop-app-logo.svg create mode 100644 apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx diff --git a/apps/web/public/images/apps/csv-airdrop-app-logo.svg b/apps/web/public/images/apps/csv-airdrop-app-logo.svg new file mode 100644 index 0000000000..5c75c1cc3d --- /dev/null +++ b/apps/web/public/images/apps/csv-airdrop-app-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx new file mode 100644 index 0000000000..27b9977ac4 --- /dev/null +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx @@ -0,0 +1,50 @@ +import ModalDialog from '@/components/common/ModalDialog' +import CSVAirdropLogo from '@/public/images/apps/csv-airdrop-app-logo.svg' +import { Button, DialogActions, DialogContent, Grid, Typography } from '@mui/material' +import router from 'next/router' +import type { ReactElement } from 'react' + +const CSVAirdropAppModal = ({ + onClose, + appUrl +}: { + onClose: () => void, + appUrl?: string +}): ReactElement => { + + const openApp = () => { + if (appUrl) { + router.push(`/apps/open?appUrl=${decodeURIComponent(appUrl)}`) + } + } + + return ( + + + + + + Use CSV Airdrop + + + You've reached the limit of 5 recipients. To add more use CSV Airdrop, where you can simply upload you CSV file and send to endless number of recipients. + + + + + + + + ) +} + +export default CSVAirdropAppModal diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx index 38f14c2e8d..08995e0b4e 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -2,14 +2,17 @@ import TokenIcon from '@/components/common/TokenIcon' import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import TxCard from '@/components/tx-flow/common/TxCard' import commonCss from '@/components/tx-flow/common/styles.module.css' +import { SafeAppsName } from '@/config/constants' +import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' import AddIcon from '@/public/images/common/add.svg' import { formatVisualAmount } from '@/utils/formatters' -import { Alert, Button, CardActions, Divider, Grid, SvgIcon, Typography } from '@mui/material' +import { Alert, Button, CardActions, Divider, Grid, Link, SvgIcon, Typography } from '@mui/material' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import { useContext, useEffect, useState, type ReactElement } from 'react' import { FormProvider, useFieldArray, useForm } from 'react-hook-form' import { TokenTransfersFields, type TokenTransfersParams } from '.' +import CSVAirdropAppModal from './CSVAirdropAppModal' import RecipientRow from './RecipientRow' export const AutocompleteItem = (item: { tokenInfo: TokenInfo; balance: string }): ReactElement => ( @@ -48,8 +51,12 @@ export const CreateTokenTransfers = ({ onSubmit: (data: TokenTransfersParams) => void txNonce?: number }): ReactElement => { + const [csvAirdropModalOpen, setCsvAirdropModalOpen] = useState(false) const [maxRecipientsAlert, setMaxRecipientsAlert] = useState(false) const { setNonce, setNonceNeeded } = useContext(SafeTxContext) + const [safeApps] = useRemoteSafeApps({ name: SafeAppsName.CSV }) + + const maxRecipients = 5 useEffect(() => { if (txNonce !== undefined) { @@ -81,6 +88,11 @@ export const CreateTokenTransfers = ({ } const addRecipient = (): void => { + if (recipientFields.length === maxRecipients) { + setCsvAirdropModalOpen(true) + return + } + if (recipientFields.length === 1) { setMaxRecipientsAlert(true) } @@ -94,7 +106,7 @@ export const CreateTokenTransfers = ({ useEffect(() => { setNonceNeeded(true) }, [setNonceNeeded]) - + return ( @@ -117,7 +129,16 @@ export const CreateTokenTransfers = ({ sx={{ mb: 2 }} onClose={() => setMaxRecipientsAlert(false)} > - If you want to add more than 5 recipients, use CSV Airdrop. + + If you want to add more than {maxRecipients} recipients, use{' '} + setCsvAirdropModalOpen(true)} + > + CSV Airdrop + + {' '}. + )} @@ -132,19 +153,17 @@ export const CreateTokenTransfers = ({ mb: 4 }} > - {recipientFields.length < 5 && ( - - )} + - {`${recipientFields.length}/5`} + {`${recipientFields.length}/${maxRecipients}`} @@ -157,6 +176,13 @@ export const CreateTokenTransfers = ({
+ + {csvAirdropModalOpen && ( + setCsvAirdropModalOpen(false)} + appUrl={safeApps?.[0]?.url} + /> + )} ) } From 7832110397f7540c55880dfdf1d261be15e6bd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Fri, 17 Jan 2025 09:05:11 +0000 Subject: [PATCH 10/21] Update progress bar styles --- .../components/common/ProgressBar/index.tsx | 14 ++++++++++++-- .../common/ProgressBar/styles.module.css | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/common/ProgressBar/index.tsx b/apps/web/src/components/common/ProgressBar/index.tsx index 4faa499c0e..2a314d4be9 100644 --- a/apps/web/src/components/common/ProgressBar/index.tsx +++ b/apps/web/src/components/common/ProgressBar/index.tsx @@ -1,8 +1,18 @@ -import { LinearProgress } from '@mui/material' +import useIsSafenetEnabled from '@/hooks/useIsSafenetEnabled' import type { LinearProgressProps } from '@mui/material' +import { LinearProgress } from '@mui/material' import css from './styles.module.css' export const ProgressBar = (props: LinearProgressProps) => { - return + const isSafenetEnabled = useIsSafenetEnabled() + + return ( + + ) } diff --git a/apps/web/src/components/common/ProgressBar/styles.module.css b/apps/web/src/components/common/ProgressBar/styles.module.css index bdc2c6a2eb..0230e2531c 100644 --- a/apps/web/src/components/common/ProgressBar/styles.module.css +++ b/apps/web/src/components/common/ProgressBar/styles.module.css @@ -1,16 +1,29 @@ -.progressBar { +.progressBar, +.safenetProgressBar { height: 6px; border-radius: 6px; +} + +.progressBar, +.safenetProgressBar { background-color: var(--color-border-light); } +.progressBar :global .MuiLinearProgress-bar, +.safenetProgressBar :global .MuiLinearProgress-bar { + border-radius: 6px; +} + .progressBar :global .MuiLinearProgress-bar { background: var(--color-primary-main); - border-radius: 6px; +} +.safenetProgressBar :global .MuiLinearProgress-bar { + background: linear-gradient(90deg, #32f970 0%, #eed509 100%); } @media (max-width: 599.95px) { - .progressBar { + .progressBar, + .safenetProgressBar { border-radius: 0; } } From a587f9cdd39e0128bfdb5c0ea643af391d06b75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Fri, 17 Jan 2025 10:40:01 +0000 Subject: [PATCH 11/21] Code formatting --- .../common/TokenAmountInput/index.tsx | 4 +- .../CSVAirdropAppModal/index.tsx | 14 ++--- .../CreateTokenTransfers.tsx | 51 ++++++------------- .../RecipientRow/index.tsx | 20 +++++--- .../ReviewTokenTransfers.tsx | 34 +++++++------ .../flows/SafenetTokenTransfers/index.tsx | 21 ++++---- .../flows/SafenetTokenTransfers/utils.ts | 7 +-- .../flows/TokenTransfer/SendAmountBlock.tsx | 4 +- apps/web/src/config/constants.ts | 4 +- 9 files changed, 71 insertions(+), 88 deletions(-) diff --git a/apps/web/src/components/common/TokenAmountInput/index.tsx b/apps/web/src/components/common/TokenAmountInput/index.tsx index 7e9916aa14..7302d2d25c 100644 --- a/apps/web/src/components/common/TokenAmountInput/index.tsx +++ b/apps/web/src/components/common/TokenAmountInput/index.tsx @@ -29,7 +29,7 @@ const TokenAmountInput = ({ }) => { const fields = { tokenAddress: groupName ? `${groupName}.${TokenAmountFields.tokenAddress}` : TokenAmountFields.tokenAddress, - amount: groupName ? `${groupName}.${TokenAmountFields.amount}` : TokenAmountFields.amount + amount: groupName ? `${groupName}.${TokenAmountFields.amount}` : TokenAmountFields.amount, } const { @@ -66,7 +66,7 @@ const TokenAmountInput = ({ fullWidth > - {errors[fields.tokenAddress]?.message || errors[fields.amount]?.message || 'Amount' } + {errors[fields.tokenAddress]?.message || errors[fields.amount]?.message || 'Amount'}
void, - appUrl?: string -}): ReactElement => { - +const CSVAirdropAppModal = ({ onClose, appUrl }: { onClose: () => void; appUrl?: string }): ReactElement => { const openApp = () => { if (appUrl) { router.push(`/apps/open?appUrl=${decodeURIComponent(appUrl)}`) @@ -34,11 +27,12 @@ const CSVAirdropAppModal = ({ Use CSV Airdrop - You've reached the limit of 5 recipients. To add more use CSV Airdrop, where you can simply upload you CSV file and send to endless number of recipients. + You've reached the limit of 5 recipients. To add more use CSV Airdrop, where you can simply upload you CSV + file and send to endless number of recipients. - + diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx index 08995e0b4e..92c15e627e 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -72,21 +72,14 @@ export const CreateTokenTransfers = ({ delayError: 500, }) - const { - handleSubmit, - control - } = formMethods + const { handleSubmit, control } = formMethods - const { - fields: recipientFields, - append, - remove, - } = useFieldArray({ control, name: TokenTransfersFields.recipients }) + const { fields: recipientFields, append, remove } = useFieldArray({ control, name: TokenTransfersFields.recipients }) const removeRecipient = (index: number): void => { remove(index) } - + const addRecipient = (): void => { if (recipientFields.length === maxRecipients) { setCsvAirdropModalOpen(true) @@ -99,7 +92,7 @@ export const CreateTokenTransfers = ({ append({ recipient: '', tokenAddress: ZERO_ADDRESS, - amount: '' + amount: '', }) } @@ -120,39 +113,30 @@ export const CreateTokenTransfers = ({ groupName={TokenTransfersFields.recipients} remove={removeRecipient} /> - {((i < recipientFields.length - 1) || (maxRecipientsAlert && i === 0)) && ( - - )} + {(i < recipientFields.length - 1 || (maxRecipientsAlert && i === 0)) && } {maxRecipientsAlert && i === 0 && ( - setMaxRecipientsAlert(false)} - > + setMaxRecipientsAlert(false)}> If you want to add more than {maxRecipients} recipients, use{' '} - setCsvAirdropModalOpen(true)} - > + setCsvAirdropModalOpen(true)}> CSV Airdrop - - {' '}. + {' '} + . )} ))} - + > - - {`${recipientFields.length}/${maxRecipients}`} - + {`${recipientFields.length}/${maxRecipients}`} @@ -176,12 +158,9 @@ export const CreateTokenTransfers = ({ - + {csvAirdropModalOpen && ( - setCsvAirdropModalOpen(false)} - appUrl={safeApps?.[0]?.url} - /> + setCsvAirdropModalOpen(false)} appUrl={safeApps?.[0]?.url} /> )} ) diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx index a65e425b16..0ce31dd7a2 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx @@ -10,7 +10,7 @@ export const RecipientRow = ({ index, groupName, removable = true, - remove + remove, }: { index: number removable?: boolean @@ -18,11 +18,11 @@ export const RecipientRow = ({ remove?: (index: number) => void }) => { const balancesItems = useVisibleTokens() - + const fieldName = `${groupName}.${index}` const { watch, - formState: { errors } + formState: { errors }, } = useFormContext() const recipient = watch(TokenTransfersFields.recipients) @@ -30,7 +30,7 @@ export const RecipientRow = ({ // TODO: Review tokens available for selection and max amount const selectedToken = balancesItems.find((item) => item.tokenInfo.symbol === 'USDC') const { maxAmount } = useTokenAmount(selectedToken) - + const isAddressValid = !!recipient && !errors[TokenTransfersFields.recipients]?.[index]?.recipient return ( @@ -41,27 +41,31 @@ export const RecipientRow = ({ sx={{ display: 'flex', alignItems: 'center', - justifyContent: 'space-between' + justifyContent: 'space-between', }} > {`Recipient ${index > 0 ? index + 1 : ''}`} {removable && ( <> - remove?.(index)} aria-label="Remove recipient"> + remove?.(index)} + aria-label="Remove recipient" + > )} - + diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx index 46299da2c0..a2639578ba 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx @@ -29,22 +29,24 @@ const ReviewTokenTransfers = ({ setNonce(txNonce) } - const calls = params.recipients.map(recipient => { - const token = balances.items.find((item) => item.tokenInfo.address === recipient.tokenAddress) - - if (!token) return + const calls = params.recipients + .map((recipient) => { + const token = balances.items.find((item) => item.tokenInfo.address === recipient.tokenAddress) + + if (!token) return - return createTokenTransferParams( - recipient.recipient, - recipient.amount, - token?.tokenInfo.decimals, - recipient.tokenAddress, - ) - }).filter((transfer): transfer is MetaTransactionData => !!transfer) + return createTokenTransferParams( + recipient.recipient, + recipient.amount, + token?.tokenInfo.decimals, + recipient.tokenAddress, + ) + }) + .filter((transfer): transfer is MetaTransactionData => !!transfer) createMultiSendCallOnlyTx(calls).then(setSafeTx).catch(setSafeTxError) }, [params, txNonce, setNonce, setSafeTx, setSafeTxError]) - + return ( {params.recipients.map((recipient, index) => { @@ -54,16 +56,18 @@ const ReviewTokenTransfers = ({ return ( <> {index > 0 && } - + - {token && } + {token && ( + + )} diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx index 25780c341a..3df76f5ab0 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/index.tsx @@ -7,7 +7,7 @@ import CreateTokenTransfers from './CreateTokenTransfers' import ReviewTokenTransfers from './ReviewTokenTransfers' enum Fields { - recipient = 'recipient' + recipient = 'recipient', } export const TokenTransferFields = { ...Fields, ...TokenAmountFields } @@ -19,7 +19,7 @@ export type TokenTransferParams = { } enum TransfersFields { - recipients = 'recipients' + recipients = 'recipients', } export const TokenTransfersFields = { ...TransfersFields } @@ -33,18 +33,19 @@ type TokenTransferFlowProps = Partial & { } const defaultParams: TokenTransfersParams = { - recipients: [{ - recipient: '', - tokenAddress: ZERO_ADDRESS, - amount: '' - }] + recipients: [ + { + recipient: '', + tokenAddress: ZERO_ADDRESS, + amount: '', + }, + ], } const SafenetTokenTransfersFlow = ({ txNonce, ...params }: TokenTransferFlowProps) => { - const { data, step, nextStep, prevStep } = useTxStepper({ ...defaultParams, - ...params + ...params, }) const steps = [ @@ -54,7 +55,7 @@ const SafenetTokenTransfersFlow = ({ txNonce, ...params }: TokenTransferFlowProp txNonce={txNonce} onSubmit={(formData) => nextStep({ ...data, ...formData })} />, - null} /> + null} />, ] return ( diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts index 865ccb04bb..ad5d00ce19 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/utils.ts @@ -2,12 +2,13 @@ import { useVisibleBalances } from '@/hooks/useVisibleBalances' import { type SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' export const useTokenAmount = (selectedToken: SafeBalanceResponse['items'][0] | undefined) => { - const totalFiatAmount = BigInt(selectedToken?.fiatBalance || 0) * (10n ** BigInt(selectedToken?.tokenInfo.decimals || 1)) + const totalFiatAmount = + BigInt(selectedToken?.fiatBalance || 0) * 10n ** BigInt(selectedToken?.tokenInfo.decimals || 1) const maxFiatAllowed = 100_000n // TODO: Fix maxFiatAllowed below to take fiatConversion into account. - const maxAmountAllowed = maxFiatAllowed * (10n ** BigInt(selectedToken?.tokenInfo.decimals || 1)) - + const maxAmountAllowed = maxFiatAllowed * 10n ** BigInt(selectedToken?.tokenInfo.decimals || 1) + const maxAmount = totalFiatAmount >= maxAmountAllowed ? maxAmountAllowed : totalFiatAmount return { maxAmount } diff --git a/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx b/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx index 55058541d1..f6d33458da 100644 --- a/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx +++ b/apps/web/src/components/tx-flow/flows/TokenTransfer/SendAmountBlock.tsx @@ -17,8 +17,8 @@ const SendAmountBlock = ({ amountInWei: number | string tokenInfo: Omit & { logoUri?: string } children?: ReactNode - title?: string, - safenet?: boolean, + title?: string + safenet?: boolean tokenSize?: number }) => { return ( diff --git a/apps/web/src/config/constants.ts b/apps/web/src/config/constants.ts index 8ff2838da2..dc8f191d8c 100644 --- a/apps/web/src/config/constants.ts +++ b/apps/web/src/config/constants.ts @@ -61,12 +61,12 @@ export enum SafeAppsTag { TX_BUILDER = 'transaction-builder', SAFE_GOVERNANCE_APP = 'safe-governance-app', ONRAMP = 'onramp', - RECOVERY_SYGNUM = 'recovery-sygnum' + RECOVERY_SYGNUM = 'recovery-sygnum', } // Safe Apps names export enum SafeAppsName { - CSV = 'CSV Airdrop' + CSV = 'CSV Airdrop', } // Help Center From f22521d7df75033737d9f0f73878317c1f7a3c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Fri, 17 Jan 2025 10:49:20 +0000 Subject: [PATCH 12/21] Fix ESLint errors --- .../components/common/TokenAmountInput/index.tsx | 15 +++++++++------ .../src/components/tx-flow/flows/NewTx/index.tsx | 2 +- .../CSVAirdropAppModal/index.tsx | 4 ++-- .../CreateTokenTransfers.tsx | 2 +- .../SafenetTokenTransfers/RecipientRow/index.tsx | 3 ++- .../ReviewTokenTransfers.tsx | 4 ++-- apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts | 2 +- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/common/TokenAmountInput/index.tsx b/apps/web/src/components/common/TokenAmountInput/index.tsx index 7302d2d25c..30da134ba4 100644 --- a/apps/web/src/components/common/TokenAmountInput/index.tsx +++ b/apps/web/src/components/common/TokenAmountInput/index.tsx @@ -5,7 +5,7 @@ import { validateDecimalLength, validateLimitedAmount } from '@/utils/validation import { Button, Divider, FormControl, InputLabel, MenuItem, TextField } from '@mui/material' import { type SafeBalanceResponse } from '@safe-global/safe-gateway-typescript-sdk' import classNames from 'classnames' -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { useFormContext } from 'react-hook-form' import css from './styles.module.css' @@ -27,10 +27,13 @@ const TokenAmountInput = ({ validate?: (value: string) => string | undefined groupName?: string }) => { - const fields = { - tokenAddress: groupName ? `${groupName}.${TokenAmountFields.tokenAddress}` : TokenAmountFields.tokenAddress, - amount: groupName ? `${groupName}.${TokenAmountFields.amount}` : TokenAmountFields.amount, - } + const fields = useMemo( + () => ({ + tokenAddress: groupName ? `${groupName}.${TokenAmountFields.tokenAddress}` : TokenAmountFields.tokenAddress, + amount: groupName ? `${groupName}.${TokenAmountFields.amount}` : TokenAmountFields.amount, + }), + [groupName], + ) const { formState: { errors }, @@ -57,7 +60,7 @@ const TokenAmountInput = ({ setValue(fields.amount, safeFormatUnits(maxAmount.toString(), selectedToken.tokenInfo.decimals), { shouldValidate: true, }) - }, [maxAmount, selectedToken, setValue]) + }, [maxAmount, selectedToken, setValue, fields]) return ( { return } setTxFlow() - }, [setTxFlow]) + }, [setTxFlow, isSafenetEnabled]) const progress = 10 diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx index 752e60f483..38e5fc665b 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx @@ -27,8 +27,8 @@ const CSVAirdropAppModal = ({ onClose, appUrl }: { onClose: () => void; appUrl?: Use CSV Airdrop - You've reached the limit of 5 recipients. To add more use CSV Airdrop, where you can simply upload you CSV - file and send to endless number of recipients. + You've reached the limit of 5 recipients. To add more use CSV Airdrop, where you can simply upload you + CSV file and send to endless number of recipients. diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx index 92c15e627e..9705b8fc45 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -115,7 +115,7 @@ export const CreateTokenTransfers = ({ /> {(i < recipientFields.length - 1 || (maxRecipientsAlert && i === 0)) && } {maxRecipientsAlert && i === 0 && ( - setMaxRecipientsAlert(false)}> + setMaxRecipientsAlert(false)}> If you want to add more than {maxRecipients} recipients, use{' '} setCsvAirdropModalOpen(true)}> diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx index 0ce31dd7a2..fc4d216c0f 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/RecipientRow/index.tsx @@ -3,7 +3,8 @@ import TokenAmountInput from '@/components/common/TokenAmountInput' import DeleteIcon from '@/public/images/common/delete.svg' import { FormControl, Grid, IconButton, SvgIcon, Typography } from '@mui/material' import { useFormContext } from 'react-hook-form' -import { TokenTransfersFields, TokenTransfersParams } from '..' +import type { TokenTransfersParams } from '..' +import { TokenTransfersFields } from '..' import { useTokenAmount, useVisibleTokens } from '../utils' export const RecipientRow = ({ diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx index a2639578ba..512720412b 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/ReviewTokenTransfers.tsx @@ -8,7 +8,7 @@ import { createTokenTransferParams } from '@/services/tx/tokenTransferParams' import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' import { safeParseUnits } from '@/utils/formatters' import { Divider, Grid } from '@mui/material' -import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { useContext, useEffect } from 'react' import type { TokenTransfersParams } from '.' @@ -45,7 +45,7 @@ const ReviewTokenTransfers = ({ .filter((transfer): transfer is MetaTransactionData => !!transfer) createMultiSendCallOnlyTx(calls).then(setSafeTx).catch(setSafeTxError) - }, [params, txNonce, setNonce, setSafeTx, setSafeTxError]) + }, [params, txNonce, setNonce, balances, setSafeTx, setSafeTxError]) return ( diff --git a/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts b/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts index a91856809d..c209595f25 100644 --- a/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts +++ b/apps/web/src/hooks/safe-apps/useRemoteSafeApps.ts @@ -1,4 +1,4 @@ -import { SafeAppsName, SafeAppsTag } from '@/config/constants' +import type { SafeAppsName, SafeAppsTag } from '@/config/constants' import useChainId from '@/hooks/useChainId' import { Errors, logError } from '@/services/exceptions' import type { SafeAppsResponse } from '@safe-global/safe-gateway-typescript-sdk' From f466425c4d760f4031afae4effff6f998be4cdb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Fri, 17 Jan 2025 11:58:56 +0000 Subject: [PATCH 13/21] Fix error message type --- apps/web/src/components/common/TokenAmountInput/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/common/TokenAmountInput/index.tsx b/apps/web/src/components/common/TokenAmountInput/index.tsx index 30da134ba4..6abcd72485 100644 --- a/apps/web/src/components/common/TokenAmountInput/index.tsx +++ b/apps/web/src/components/common/TokenAmountInput/index.tsx @@ -69,7 +69,7 @@ const TokenAmountInput = ({ fullWidth > - {errors[fields.tokenAddress]?.message || errors[fields.amount]?.message || 'Amount'} + {errors[fields.tokenAddress]?.message?.toString() || errors[fields.amount]?.message?.toString() || 'Amount'}
Date: Mon, 20 Jan 2025 11:23:01 +0000 Subject: [PATCH 14/21] Update apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx Co-authored-by: Manuel Gellfart --- .../flows/SafenetTokenTransfers/CreateTokenTransfers.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx index 9705b8fc45..dbddd02f42 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -74,11 +74,7 @@ export const CreateTokenTransfers = ({ const { handleSubmit, control } = formMethods - const { fields: recipientFields, append, remove } = useFieldArray({ control, name: TokenTransfersFields.recipients }) - - const removeRecipient = (index: number): void => { - remove(index) - } + const { fields: recipientFields, append, remove: removeRecipient } = useFieldArray({ control, name: TokenTransfersFields.recipients }) const addRecipient = (): void => { if (recipientFields.length === maxRecipients) { From 3016d1e25ae04d8264b8d09c7009a793a0c5405b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Mon, 20 Jan 2025 12:00:54 +0000 Subject: [PATCH 15/21] Fix CSV Airdrop app link --- .../CSVAirdropAppModal/index.tsx | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx index 38e5fc665b..f20d2af882 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CSVAirdropAppModal/index.tsx @@ -1,15 +1,13 @@ import ModalDialog from '@/components/common/ModalDialog' +import { AppRoutes } from '@/config/routes' import CSVAirdropLogo from '@/public/images/apps/csv-airdrop-app-logo.svg' import { Button, DialogActions, DialogContent, Grid, Typography } from '@mui/material' -import router from 'next/router' +import Link from 'next/link' +import { useRouter } from 'next/router' import type { ReactElement } from 'react' const CSVAirdropAppModal = ({ onClose, appUrl }: { onClose: () => void; appUrl?: string }): ReactElement => { - const openApp = () => { - if (appUrl) { - router.push(`/apps/open?appUrl=${decodeURIComponent(appUrl)}`) - } - } + const router = useRouter() return ( void; appUrl?: - - - + {appUrl && ( + + + + + + )} ) } From 7a0bb147f382c935caa4eafb40ea6c508123933b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Mon, 20 Jan 2025 12:01:23 +0000 Subject: [PATCH 16/21] Remove setNonceNeeded from Safenet token transfers --- .../flows/SafenetTokenTransfers/CreateTokenTransfers.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx index dbddd02f42..9f709c0160 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -53,7 +53,7 @@ export const CreateTokenTransfers = ({ }): ReactElement => { const [csvAirdropModalOpen, setCsvAirdropModalOpen] = useState(false) const [maxRecipientsAlert, setMaxRecipientsAlert] = useState(false) - const { setNonce, setNonceNeeded } = useContext(SafeTxContext) + const { setNonce } = useContext(SafeTxContext) const [safeApps] = useRemoteSafeApps({ name: SafeAppsName.CSV }) const maxRecipients = 5 @@ -92,10 +92,6 @@ export const CreateTokenTransfers = ({ }) } - useEffect(() => { - setNonceNeeded(true) - }, [setNonceNeeded]) - return ( From ea0a4571ca473bdb1291cab2e1e10dbae6ab7e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADnez?= Date: Mon, 20 Jan 2025 12:04:21 +0000 Subject: [PATCH 17/21] Replace Grid with Stack --- .../SafenetTokenTransfers/CreateTokenTransfers.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx index 9f709c0160..2a52f1f772 100644 --- a/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx +++ b/apps/web/src/components/tx-flow/flows/SafenetTokenTransfers/CreateTokenTransfers.tsx @@ -6,7 +6,7 @@ import { SafeAppsName } from '@/config/constants' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' import AddIcon from '@/public/images/common/add.svg' import { formatVisualAmount } from '@/utils/formatters' -import { Alert, Button, CardActions, Divider, Grid, Link, SvgIcon, Typography } from '@mui/material' +import { Alert, Button, CardActions, Divider, Grid, Link, Stack, SvgIcon, Typography } from '@mui/material' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' import { useContext, useEffect, useState, type ReactElement } from 'react' @@ -120,15 +120,7 @@ export const CreateTokenTransfers = ({ ))} - +
diff --git a/apps/web/src/components/tx/confirmation-views/__snapshots__/ConfirmationView.test.tsx.snap b/apps/web/src/components/tx/confirmation-views/__snapshots__/ConfirmationView.test.tsx.snap index 3cc1bdfc79..e2c961c404 100644 --- a/apps/web/src/components/tx/confirmation-views/__snapshots__/ConfirmationView.test.tsx.snap +++ b/apps/web/src/components/tx/confirmation-views/__snapshots__/ConfirmationView.test.tsx.snap @@ -284,14 +284,18 @@ exports[`ConfirmationView should display a confirmation screen for a SETTINGS_CH
- ETH +
+ ETH +

@@ -791,14 +795,18 @@ exports[`ConfirmationView should display a confirmation with method call when th

- ETH +
+ ETH +