From 9a3a19861a08a4db59da57df986075475ca405ce Mon Sep 17 00:00:00 2001 From: hotequil Date: Mon, 20 Jan 2025 09:57:36 -0300 Subject: [PATCH] CU-86a69431g-NEON3 - Implement an icon to paste values into address fields --- src/renderer/src/components/Input.tsx | 3 ++ src/renderer/src/components/Textarea.tsx | 9 +++- src/renderer/src/hooks/usePressOnce.ts | 24 +++++++++ src/renderer/src/locales/en/common.json | 4 +- .../src/routes/modals/AddAddress/index.tsx | 1 + .../routes/modals/AddCustomNetwork/index.tsx | 1 + .../routes/modals/DappConnection/index.tsx | 1 + .../src/routes/modals/Import/index.tsx | 1 + .../src/routes/pages/LoginKey/index.tsx | 1 + .../src/routes/pages/Send/SendRecipient.tsx | 1 + .../Settings/SettingsEncryptKey/index.tsx | 1 + .../src/routes/pages/Swap/SwapPageContent.tsx | 54 ++++++++++++++----- src/shared/@types/i18next-resources.d.ts | 2 + 13 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 src/renderer/src/hooks/usePressOnce.ts diff --git a/src/renderer/src/components/Input.tsx b/src/renderer/src/components/Input.tsx index daf9a9bf..be63609c 100644 --- a/src/renderer/src/components/Input.tsx +++ b/src/renderer/src/components/Input.tsx @@ -1,4 +1,5 @@ import { cloneElement, forwardRef, MouseEvent, useImperativeHandle, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { MdCancel, MdContentCopy, MdContentPasteGo, MdVisibility, MdVisibilityOff } from 'react-icons/md' import { StyleHelper } from '@renderer/helpers/StyleHelper' import { TestHelper } from '@renderer/helpers/TestHelper' @@ -50,6 +51,7 @@ export const Input = forwardRef( }, ref ) => { + const { t: tCommonGeneral } = useTranslation('common', { keyPrefix: 'general' }) const isTypePassword = type === 'password' const internalRef = useRef(null) const [hidden, setHidden] = useState(isTypePassword) @@ -172,6 +174,7 @@ export const Input = forwardRef( {pastable && ( } onClick={handlePaste} colorSchema="neon" diff --git a/src/renderer/src/components/Textarea.tsx b/src/renderer/src/components/Textarea.tsx index a0ce35e6..dd31a368 100644 --- a/src/renderer/src/components/Textarea.tsx +++ b/src/renderer/src/components/Textarea.tsx @@ -1,4 +1,5 @@ import { ChangeEventHandler, forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react' +import { useTranslation } from 'react-i18next' import { MdCancel, MdContentPasteGo } from 'react-icons/md' import { StyleHelper } from '@renderer/helpers/StyleHelper' @@ -30,6 +31,7 @@ export const Textarea = forwardRef( }, ref ) => { + const { t: tCommonGeneral } = useTranslation('common', { keyPrefix: 'general' }) const internalRef = useRef(null) const handlePaste = async () => { @@ -117,10 +119,13 @@ export const Textarea = forwardRef( {pastable && ( } - onClick={handlePaste} + aria-label={tCommonGeneral('pasteFromClipboard')} type="button" + colorSchema="neon" compacted + disabled={props.disabled} + icon={} + onClick={handlePaste} /> )} diff --git a/src/renderer/src/hooks/usePressOnce.ts b/src/renderer/src/hooks/usePressOnce.ts new file mode 100644 index 00000000..a797d435 --- /dev/null +++ b/src/renderer/src/hooks/usePressOnce.ts @@ -0,0 +1,24 @@ +import { useRef, useState } from 'react' + +export const usePressOnce = () => { + const [isPressing, setIsPressing] = useState(false) + const isPressingRef = useRef(false) + + const handlePressOnce = (callback: (() => void) | (() => Promise)) => async () => { + if (isPressing || isPressingRef.current) return + + setIsPressing(true) + isPressingRef.current = true + + try { + await callback() + } catch (error) { + console.error(error) + } finally { + setIsPressing(false) + isPressingRef.current = false + } + } + + return { isPressing, isPressingRef, handlePressOnce } +} diff --git a/src/renderer/src/locales/en/common.json b/src/renderer/src/locales/en/common.json index 0e5901f2..aac43dc2 100644 --- a/src/renderer/src/locales/en/common.json +++ b/src/renderer/src/locales/en/common.json @@ -15,7 +15,9 @@ "remove": "Remove", "copy": "Copy to clipboard", "swap": "Swap", - "logo": "Neon Wallet's logo" + "logo": "Neon Wallet's logo", + "pasteFromClipboard": "Paste from clipboard", + "pasteFromClipboardError": "Failed to paste from clipboard" }, "walletConnect": { "name": "Neon Wallet", diff --git a/src/renderer/src/routes/modals/AddAddress/index.tsx b/src/renderer/src/routes/modals/AddAddress/index.tsx index b0ece2af..8fa7b0c7 100644 --- a/src/renderer/src/routes/modals/AddAddress/index.tsx +++ b/src/renderer/src/routes/modals/AddAddress/index.tsx @@ -99,6 +99,7 @@ export const AddAddressModal = () => { onChange={handleChange} clearable compacted + pastable loading={isValidatingAddressOrDomainAddress} disabled={!actionData.blockchain} error={isValidAddressOrDomainAddress === false} diff --git a/src/renderer/src/routes/modals/AddCustomNetwork/index.tsx b/src/renderer/src/routes/modals/AddCustomNetwork/index.tsx index 1a4558f1..e3e536f2 100644 --- a/src/renderer/src/routes/modals/AddCustomNetwork/index.tsx +++ b/src/renderer/src/routes/modals/AddCustomNetwork/index.tsx @@ -113,6 +113,7 @@ export const AddCustomNetwork = () => { compacted placeholder={t('urlPlaceholder')} label={t('urlLabel')} + pastable value={actionData.url} onChange={setDataFromEventWrapper('url')} loading={actionData.validating} diff --git a/src/renderer/src/routes/modals/DappConnection/index.tsx b/src/renderer/src/routes/modals/DappConnection/index.tsx index 3a2e1732..4c8e12f3 100644 --- a/src/renderer/src/routes/modals/DappConnection/index.tsx +++ b/src/renderer/src/routes/modals/DappConnection/index.tsx @@ -84,6 +84,7 @@ export const DappConnectionModal = () => { { onChange={handleChange} compacted clearable + pastable multiline={actionData.inputType === 'mnemonic'} /> diff --git a/src/renderer/src/routes/pages/LoginKey/index.tsx b/src/renderer/src/routes/pages/LoginKey/index.tsx index 9b1dc467..f87cc89c 100644 --- a/src/renderer/src/routes/pages/LoginKey/index.tsx +++ b/src/renderer/src/routes/pages/LoginKey/index.tsx @@ -59,6 +59,7 @@ export const LoginKeyPage = () => { value={actionData.text} onChange={handleChange} clearable + pastable multiline={actionData.inputType === 'mnemonic'} {...TestHelper.buildTestObject('login-key-textarea')} /> diff --git a/src/renderer/src/routes/pages/Send/SendRecipient.tsx b/src/renderer/src/routes/pages/Send/SendRecipient.tsx index b6ef7e39..8827513e 100644 --- a/src/renderer/src/routes/pages/Send/SendRecipient.tsx +++ b/src/renderer/src/routes/pages/Send/SendRecipient.tsx @@ -151,6 +151,7 @@ export const SendRecipient = ({ className="w-full" placeholder={t('addressPlaceholder')} clearable={false} + pastable buttons={ } diff --git a/src/renderer/src/routes/pages/Settings/SettingsEncryptKey/index.tsx b/src/renderer/src/routes/pages/Settings/SettingsEncryptKey/index.tsx index 5b1df98a..b231f48c 100644 --- a/src/renderer/src/routes/pages/Settings/SettingsEncryptKey/index.tsx +++ b/src/renderer/src/routes/pages/Settings/SettingsEncryptKey/index.tsx @@ -110,6 +110,7 @@ export const SettingsEncryptKeyPage = (): JSX.Element => {
{ const { t } = useTranslation('pages', { keyPrefix: 'swap' }) + const { t: tCommonGeneral } = useTranslation('common', { keyPrefix: 'general' }) const { modalNavigateWrapper, modalNavigate } = useModalNavigate() const { networkByBlockchain } = useSelectedNetworkByBlockchainSelector() const { currentLoginSessionRef } = useCurrentLoginSessionSelector() const { accounts } = useAccountsSelector() const dispatch = useAppDispatch() + const pressOncePasteAddressToReceive = usePressOnce() const swapChainsByServiceName = useMemo(() => { const chainsByServiceName: Partial> = {} @@ -277,6 +280,17 @@ export const SwapPageContent = ({ account }: TProps) => { swapServiceRef.current.setExtraIdToReceive(event.target.value) } + const handlePasteAddressToReceive = async () => { + try { + const text = await navigator.clipboard.readText() + + await swapServiceRef.current!.setAddressToReceive(text) + } catch (error) { + ToastHelper.error({ message: tCommonGeneral('pasteFromClipboardError') }) + console.error(error) + } + } + const handleChangeAmountToUse = (event: ChangeEvent) => { const amount = NumberHelper.formatString(event.target.value, actionData.selectedTokenToUse.value?.decimals, 24) @@ -649,19 +663,31 @@ export const SwapPageContent = ({ account }: TProps) => { placeholder={t('form.addressToReceivePlaceholder')} clearable={false} buttons={ - } - colorSchema="neon" - type="button" - onClick={modalNavigateWrapper('select-contact', { - state: { - onSelectContact: handleSelectContactToReceive, - blockchain: tokenToReceiveBlockchain, - }, - })} - compacted - disabled={isContactsAndAccountsSelectionDisabled || !hasContactsByBlockchain} - /> + + } + onClick={pressOncePasteAddressToReceive.handlePressOnce(handlePasteAddressToReceive)} + /> + + } + colorSchema="neon" + type="button" + onClick={modalNavigateWrapper('select-contact', { + state: { + onSelectContact: handleSelectContactToReceive, + blockchain: tokenToReceiveBlockchain, + }, + })} + compacted + disabled={isContactsAndAccountsSelectionDisabled || !hasContactsByBlockchain} + /> + } loading={actionData.selectedAddressToReceive.loading} errorMessage={ diff --git a/src/shared/@types/i18next-resources.d.ts b/src/shared/@types/i18next-resources.d.ts index 4f0c9983..73895bca 100644 --- a/src/shared/@types/i18next-resources.d.ts +++ b/src/shared/@types/i18next-resources.d.ts @@ -17,6 +17,8 @@ interface Resources { copy: 'Copy to clipboard' swap: 'Swap' logo: "Neon Wallet's logo" + pasteFromClipboard: 'Paste from clipboard' + pasteFromClipboardError: 'Failed to paste from clipboard' } walletConnect: { name: 'Neon Wallet'