From b0e2e20b6aa70101f78bf5ed04ee077fdc42c7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Mon, 21 Oct 2024 17:42:00 -0300 Subject: [PATCH] fix(claim): validation loop on recipient input --- src/components/Claim/Link/Initial.view.tsx | 55 ++--- .../Global/GeneralRecipientInput/index.tsx | 219 ++++++------------ .../Global/ValidatedInput/index.tsx | 2 +- 3 files changed, 89 insertions(+), 187 deletions(-) diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 2930ffd6..1c9b350d 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -1,5 +1,5 @@ 'use client' -import GeneralRecipientInput from '@/components/Global/GeneralRecipientInput' +import GeneralRecipientInput, { GenerealRecipientUpdate } from '@/components/Global/GeneralRecipientInput' import * as _consts from '../Claim.consts' import { useContext, useEffect, useState } from 'react' import Icon from '@/components/Global/Icon' @@ -9,7 +9,6 @@ import useClaimLink from '../useClaimLink' import * as context from '@/context' import Loading from '@/components/Global/Loading' import * as consts from '@/constants' -import * as interfaces from '@/interfaces' import * as utils from '@/utils' import MoreInfo from '@/components/Global/MoreInfo' import TokenSelectorXChain from '@/components/Global/TokenSelector/TokenSelectorXChain' @@ -126,10 +125,10 @@ export const InitialClaimLinkView = ({ } } - const _estimatePoints = async () => { + const _estimatePoints = async (address: string) => { const USDValue = Number(claimLinkData.tokenAmount) * (tokenPrice ?? 0) const estimatedPoints = await estimatePoints({ - address: recipient.address ?? address ?? '', + address, chainId: claimLinkData.chainId, amountUSD: USDValue, actionType: ActionType.CLAIM, @@ -255,10 +254,10 @@ export const InitialClaimLinkView = ({ } useEffect(() => { - if (recipient) { - _estimatePoints() + if (recipient?.address && isValidRecipient) { + _estimatePoints(recipient.address) } - }, [recipient]) + }, [recipient.address, isValidRecipient]) useEffect(() => { if (recipient.address) return @@ -463,42 +462,16 @@ export const InitialClaimLinkView = ({ { - setRecipient({ name, address }) - setInputChanging(false) - }} - _setIsValidRecipient={({ isValid, error }: { isValid: boolean; error?: string }) => { - setIsValidRecipient(isValid) - if (error) { - setErrorState({ - showError: true, - errorMessage: error, - }) - } else { - setErrorState({ - showError: false, - errorMessage: '', - }) - } - setInputChanging(false) - }} - setIsValueChanging={() => { - setInputChanging(true) - }} - setRecipientType={(type: interfaces.RecipientType) => { - setRecipientType(type) - }} - onDeleteClick={() => { - setRecipientType('address') - setRecipient({ - name: undefined, - address: '', - }) + recipient={recipient} + onUpdate={(update: GenerealRecipientUpdate) => { + setRecipient(update.recipient) + setRecipientType(update.type) + setIsValidRecipient(update.isValid) setErrorState({ - showError: false, - errorMessage: '', + showError: !update.isValid, + errorMessage: update.errorMessage, }) + setInputChanging(update.isChanging) }} /> {recipient && isValidRecipient && recipientType !== 'iban' && recipientType !== 'us' && ( diff --git a/src/components/Global/GeneralRecipientInput/index.tsx b/src/components/Global/GeneralRecipientInput/index.tsx index c4696ecd..07f59950 100644 --- a/src/components/Global/GeneralRecipientInput/index.tsx +++ b/src/components/Global/GeneralRecipientInput/index.tsx @@ -1,183 +1,112 @@ 'use client' -import { useEffect, useState } from 'react' +import { useCallback, useRef } from 'react' import { isIBAN } from 'validator' -import Icon from '@/components/Global/Icon' -import * as interfaces from '@/interfaces' +import ValidatedInput, { InputUpdate } from '@/components/Global/ValidatedInput' import * as utils from '@/utils' import { ethers } from 'ethers' +import * as interfaces from '@/interfaces' type GeneralRecipientInputProps = { className?: string placeholder: string - value: string - onSubmit: any - _setIsValidRecipient: any - setIsValueChanging?: any - setRecipientType: any - onDeleteClick: any + recipient: { name: string | undefined; address: string } + onUpdate: (update: GenerealRecipientUpdate) => void +} + +export type GenerealRecipientUpdate = { + recipient: { name: string | undefined; address: string } + type: interfaces.RecipientType + isValid: boolean + isChanging: boolean + errorMessage: string } -const GeneralRecipientInput = ({ - placeholder, - value, - onSubmit, - _setIsValidRecipient, - setIsValueChanging, - setRecipientType, - onDeleteClick, -}: GeneralRecipientInputProps) => { - const [userInput, setUserInput] = useState(value) - const [recipient, setAddress] = useState(value) - const [deboundedRecipient, setDeboundedRecipient] = useState('') - const [isValidRecipient, setIsValidRecipient] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [type, setType] = useState('address') +const GeneralRecipientInput = ({ placeholder, recipient, onUpdate, className }: GeneralRecipientInputProps) => { + const recipientType = useRef('address') + const errorMessage = useRef('') + const resolvedAddress = useRef('') - async function checkAddress(recipient: string) { + const checkAddress = useCallback(async (recipient: string): Promise => { try { if (isIBAN(recipient)) { const validAccount = await utils.validateBankAccount(recipient) + recipientType.current = 'iban' if (validAccount) { - setIsValidRecipient(true) - _setIsValidRecipient({ isValid: true }) - setRecipientType('iban') - setType('iban') - setAddress(recipient) - onSubmit(userInput, recipient) + return true } else { - setIsValidRecipient(false) - _setIsValidRecipient({ isValid: false, error: 'Invalid IBAN, country not supported' }) + errorMessage.current = 'Invalid IBAN, country not supported' + return false } } else if (/^[0-9]{6,17}$/.test(recipient)) { const validateBankAccount = await utils.validateBankAccount(recipient) + recipientType.current = 'us' if (validateBankAccount) { - setIsValidRecipient(true) - _setIsValidRecipient({ isValid: true }) - setRecipientType('us') - setType('us') - setAddress(recipient) - onSubmit(userInput, recipient) + return true } else { - setIsValidRecipient(false) - _setIsValidRecipient({ isValid: false, error: 'Invalid US account number' }) + errorMessage.current = 'Invalid US account number' + return false } } else if (recipient.toLowerCase().endsWith('.eth')) { - const resolvedAddress = await utils.resolveFromEnsName(recipient.toLowerCase()) - if (resolvedAddress) { - recipient = resolvedAddress - setIsValidRecipient(true) - _setIsValidRecipient({ isValid: true }) - setAddress(recipient) - setRecipientType('ens') - setType('ens') - onSubmit(userInput, recipient) + const address = await utils.resolveFromEnsName(recipient.toLowerCase()) + recipientType.current = 'ens' + if (address) { + resolvedAddress.current = address + return true } else { - setIsValidRecipient(false) - _setIsValidRecipient({ isValid: false }) + errorMessage.current = 'ENS not found' + return false } } else if (ethers.utils.isAddress(recipient)) { - setAddress(recipient) - setIsValidRecipient(true) - _setIsValidRecipient({ isValid: true }) - setRecipientType('address') - setType('address') - onSubmit(undefined, recipient) + recipientType.current = 'address' + return true } else { - setIsValidRecipient(false) - _setIsValidRecipient({ isValid: false }) + recipientType.current = 'address' + errorMessage.current = 'Invalid address' + return false } } catch (error) { console.error('Error while validating recipient input field:', error) - setIsValidRecipient(false) - _setIsValidRecipient({ isValid: false }) - } finally { - setIsLoading(false) - } - } - - useEffect(() => { - if (recipient && isValidRecipient) { - _setIsValidRecipient({ isValid: true }) + return false } - }, [recipient]) + }, []) - useEffect(() => { - setIsLoading(true) - const handler = setTimeout(() => { - setDeboundedRecipient(userInput) - }, 750) - return () => { - clearTimeout(handler) - } - }, [userInput]) - - useEffect(() => { - if (deboundedRecipient) { - checkAddress(deboundedRecipient) + const onInputUpdate = useCallback((update: InputUpdate) => { + let _update: GenerealRecipientUpdate + if (update.isValid) { + errorMessage.current = '' + _update = { + recipient: + 'ens' === recipientType.current + ? { address: resolvedAddress.current, name: update.value } + : { address: update.value, name: undefined }, + type: recipientType.current, + isValid: true, + isChanging: update.isChanging, + errorMessage: '', + } + } else { + resolvedAddress.current = '' + _update = { + recipient: { address: update.value, name: undefined }, + type: recipientType.current, + isValid: false, + isChanging: update.isChanging, + errorMessage: errorMessage.current, + } } - }, [deboundedRecipient]) - - useEffect(() => { - setUserInput(value) - }, [value]) + onUpdate(_update) + }, []) return ( -
-
- To: -
- { - e.preventDefault() - }} - onChange={(e) => { - setIsValueChanging(true) - if (e.target.value) { - setUserInput(e.target.value) - } else { - _setIsValidRecipient({ isValid: false }) - setUserInput('') - } - }} - spellCheck="false" - /> - {userInput.length > 0 ? ( - isLoading ? ( -
-
-
- ) : ( - userInput && ( - - ) - ) - ) : null} -
+ ) } diff --git a/src/components/Global/ValidatedInput/index.tsx b/src/components/Global/ValidatedInput/index.tsx index dbcf8f0a..2053f239 100644 --- a/src/components/Global/ValidatedInput/index.tsx +++ b/src/components/Global/ValidatedInput/index.tsx @@ -44,7 +44,7 @@ const ValidatedInput = ({ return () => { isStale = true } - }, [debouncedValue, validate, previousValueRef]) + }, [debouncedValue]) useEffect(() => { const handler = setTimeout(() => {