Skip to content

Commit

Permalink
fix(claim): validation loop on recipient input
Browse files Browse the repository at this point in the history
  • Loading branch information
jjramirezn committed Oct 21, 2024
1 parent e3b268e commit b0e2e20
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 187 deletions.
55 changes: 14 additions & 41 deletions src/components/Claim/Link/Initial.view.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -463,42 +462,16 @@ export const InitialClaimLinkView = ({
<GeneralRecipientInput
className="px-1"
placeholder="wallet address / ENS / IBAN / US account number"
value={recipient.name ? recipient.name : (recipient.address ?? '')}
onSubmit={(name: string, address: string) => {
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' && (
Expand Down
219 changes: 74 additions & 145 deletions src/components/Global/GeneralRecipientInput/index.tsx
Original file line number Diff line number Diff line change
@@ -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<string>(value)
const [recipient, setAddress] = useState<string>(value)
const [deboundedRecipient, setDeboundedRecipient] = useState<string>('')
const [isValidRecipient, setIsValidRecipient] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [type, setType] = useState<interfaces.RecipientType>('address')
const GeneralRecipientInput = ({ placeholder, recipient, onUpdate, className }: GeneralRecipientInputProps) => {
const recipientType = useRef<interfaces.RecipientType>('address')
const errorMessage = useRef('')
const resolvedAddress = useRef('')

async function checkAddress(recipient: string) {
const checkAddress = useCallback(async (recipient: string): Promise<boolean> => {
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 (
<div
className={`border-n-1 relative w-full max-w-96 border dark:border-white${
userInput && !isLoading && isValidRecipient
? ' border-n-1 border dark:border-white'
: userInput && !isLoading && !isValidRecipient
? ' border-n-1 border-red dark:border-red'
: ''
}`}
>
<div className="text-h8 absolute left-1 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center bg-white font-medium">
To:
</div>
<input
className={`transition-color text-h8 focus:border-purple-1 dark:bg-n-1 dark:focus:border-purple-1
h-12 w-full rounded-none bg-transparent bg-white px-4 pl-8 font-medium outline-none placeholder:text-sm dark:border-white dark:text-white dark:placeholder:text-white/75`}
type="text"
placeholder={placeholder}
value={userInput}
onSubmit={(e) => {
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 ? (
<div className="absolute right-2 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center bg-white">
<div
className="h-4 w-4 animate-spin rounded-full border-2 border-solid border-current border-r-transparent motion-reduce:animate-none"
role="status"
/>
</div>
) : (
userInput && (
<button
onClick={(e) => {
e.preventDefault()
setUserInput('')
onDeleteClick()
}}
className="absolute right-2 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center bg-white"
>
<Icon className="h-6 w-6 dark:fill-white" name="close" />
</button>
)
)
) : null}
</div>
<ValidatedInput
placeholder={placeholder}
label="To"
value={recipient.name ?? recipient.address}
debounceTime={750}
validate={checkAddress}
onUpdate={onInputUpdate}
className={className}
/>
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/Global/ValidatedInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const ValidatedInput = ({
return () => {
isStale = true
}
}, [debouncedValue, validate, previousValueRef])
}, [debouncedValue])

useEffect(() => {
const handler = setTimeout(() => {
Expand Down

0 comments on commit b0e2e20

Please sign in to comment.