diff --git a/src/common/hooks/useAddressInput/useAddressInput.tsx b/src/common/hooks/useAddressInput/useAddressInput.tsx index 29b09ee60..a3df9058f 100644 --- a/src/common/hooks/useAddressInput/useAddressInput.tsx +++ b/src/common/hooks/useAddressInput/useAddressInput.tsx @@ -133,7 +133,7 @@ const useAddressInput = ({ ]) useEffect(() => { - const trimmedAddress = fieldValue.trim() + const trimmedAddress = (fieldValue || '').trim() const dotIndexInAddress = trimmedAddress.indexOf('.') // There is a dot and it is not the first or last character const canBeDomain = diff --git a/src/web/modules/PPv1/transfer/screens/TransferScreen.tsx b/src/web/modules/PPv1/transfer/screens/TransferScreen.tsx index 03bd0d68e..688e31ff8 100644 --- a/src/web/modules/PPv1/transfer/screens/TransferScreen.tsx +++ b/src/web/modules/PPv1/transfer/screens/TransferScreen.tsx @@ -122,7 +122,7 @@ const TransferScreen = () => { ) // Privacy Pools state - const controllerAmountFieldValue = amountFieldMode === 'token' ? withdrawalAmount : amountInFiat + const controllerAmountFieldValue = (amountFieldMode === 'token' ? withdrawalAmount : amountInFiat) || '' const [amountFieldValue, setAmountFieldValue] = useSyncedState({ backgroundState: controllerAmountFieldValue, updateBackgroundState: (newAmount) => { @@ -131,7 +131,7 @@ const TransferScreen = () => { forceUpdateOnChangeList: [programmaticUpdateCounter, amountFieldMode] }) const [addressStateFieldValue, setAddressStateFieldValue] = useSyncedState({ - backgroundState: addressState.fieldValue, + backgroundState: addressState.fieldValue || '', updateBackgroundState: (newAddress: string) => { handleUpdateForm({ addressState: { fieldValue: newAddress } }) }, @@ -140,7 +140,7 @@ const TransferScreen = () => { // Railgun state syncing const railgunControllerAmountFieldValue = - railgunAmountFieldMode === 'token' ? railgunWithdrawalAmount : railgunAmountInFiat + (railgunAmountFieldMode === 'token' ? railgunWithdrawalAmount : railgunAmountInFiat) || '' const [railgunAmountFieldValue, setRailgunAmountFieldValue] = useSyncedState({ backgroundState: railgunControllerAmountFieldValue, updateBackgroundState: (newAmount) => { @@ -149,7 +149,7 @@ const TransferScreen = () => { forceUpdateOnChangeList: [railgunProgrammaticUpdateCounter, railgunAmountFieldMode] }) const [railgunAddressStateFieldValue, setRailgunAddressStateFieldValue] = useSyncedState({ - backgroundState: railgunAddressState.fieldValue, + backgroundState: railgunAddressState.fieldValue || '', updateBackgroundState: (newAddress: string) => { handleRailgunUpdateForm({ addressState: { fieldValue: newAddress } }) }, @@ -382,11 +382,11 @@ const TransferScreen = () => { } const amountErrorMessage = useMemo(() => { - if (!withdrawalAmount || withdrawalAmount.trim() === '') return '' + if (!amountFieldValue || amountFieldValue.trim() === '') return '' if (!poolInfo) return '' try { - const amount = parseUnits(withdrawalAmount, 18) + const amount = parseUnits(amountFieldValue, 18) if (amount < poolInfo.minWithdrawal) { return `Minimum transfer amount is ${formatUnits(poolInfo.minWithdrawal, 18)} ETH` @@ -409,7 +409,7 @@ const TransferScreen = () => { } catch (error) { return 'Invalid amount' } - }, [withdrawalAmount, totalApprovedBalance.total, poolInfo, relayerQuote]) + }, [amountFieldValue, totalApprovedBalance.total, poolInfo, relayerQuote]) // Privacy Pools form validation const isTransferFormValid = useMemo(() => { @@ -437,11 +437,20 @@ const TransferScreen = () => { const railgunForm = useRailgunForm() const railgunTotalPrivateBalancesFormatted = railgunForm.totalPrivateBalancesFormatted - // Railgun amount error message + // Railgun amount error message - immediate validation for UX const railgunAmountErrorMessage = useMemo(() => { - if (!railgunWithdrawalAmount || railgunWithdrawalAmount.trim() === '') return '' + const amountToValidate = railgunAmountFieldMode === 'token' + ? railgunAmountFieldValue + : railgunWithdrawalAmount + + if (!amountToValidate || amountToValidate.trim() === '') return '' if (!railgunSelectedToken) return '' + // First check controller's validation (format, non-zero) if available + if (railgunWithdrawalAmount && !railgunValidationFormMsgs.amount.success) { + return railgunValidationFormMsgs.amount.message + } + try { // Get the balance for the selected token const tokenAddressLower = railgunSelectedToken.address?.toLowerCase() @@ -452,7 +461,7 @@ const TransferScreen = () => { } const decimals = balanceInfo.decimals || railgunSelectedToken.decimals || 18 - const amount = parseUnits(railgunWithdrawalAmount, decimals) + const amount = parseUnits(amountToValidate, decimals) const availableBalance = BigInt(balanceInfo.amount) if (amount > availableBalance) { @@ -463,24 +472,24 @@ const TransferScreen = () => { } catch (error) { return 'Invalid amount' } - }, [railgunWithdrawalAmount, railgunSelectedToken, railgunTotalPrivateBalancesFormatted]) + }, [ + railgunAmountFieldValue, + railgunWithdrawalAmount, + railgunAmountFieldMode, + railgunValidationFormMsgs.amount, + railgunSelectedToken, + railgunTotalPrivateBalancesFormatted + ]) // Railgun form validation const isRailgunTransferFormValid = useMemo(() => { - // Use the synced state value for address check, or fall back to controller state - const addressValue = railgunAddressStateFieldValue || railgunAddressState.fieldValue - const hasAmount = - railgunAmountFieldValue && - railgunAmountFieldValue !== '0' && - parseFloat(railgunAmountFieldValue) > 0 + // Use controller's validation (with debounce) for proper validation + const hasAmount = !!railgunWithdrawalAmount && railgunWithdrawalAmount !== '0' const hasToken = !!railgunSelectedToken - const hasAddress = addressValue && addressValue.trim() !== '' - - // For address validation, check if the address input state says it's valid - // OR if we have an address value and no explicit error - const addressIsValid = - !railgunAddressInputState.validation.isError || - (hasAddress && !railgunAddressInputState.validation.message) + const hasAddress = !!railgunAddressState.fieldValue || !!railgunAddressState.ensAddress + + // Use controller's validation messages + const addressIsValid = railgunValidationFormMsgs.recipientAddress.success const noAmountError = !railgunAmountErrorMessage // Debug logging (can be removed later) @@ -491,26 +500,23 @@ const TransferScreen = () => { hasAddress, addressIsValid, noAmountError, - amountFieldValue: railgunAmountFieldValue, - addressFieldValue: addressValue, - syncedAddressFieldValue: railgunAddressStateFieldValue, - controllerAddressFieldValue: railgunAddressState.fieldValue, + withdrawalAmount: railgunWithdrawalAmount, + addressFieldValue: railgunAddressState.fieldValue, + ensAddress: railgunAddressState.ensAddress, selectedToken: railgunSelectedToken?.symbol, - addressError: railgunAddressInputState.validation.isError, - addressValidationMessage: railgunAddressInputState.validation.message, + validationMsgs: railgunValidationFormMsgs, amountError: railgunAmountErrorMessage }) } return !!(hasAmount && hasToken && hasAddress && addressIsValid && noAmountError) }, [ - railgunAmountFieldValue, + railgunWithdrawalAmount, railgunAmountErrorMessage, railgunSelectedToken, - railgunAddressInputState.validation.isError, - railgunAddressInputState.validation.message, - railgunAddressStateFieldValue, - railgunAddressState.fieldValue + railgunValidationFormMsgs, + railgunAddressState.fieldValue, + railgunAddressState.ensAddress ]) const onBack = useCallback(() => { @@ -603,20 +609,17 @@ const TransferScreen = () => { setIsSubmitting(true) let isInternalTransfer = false try { - // Use synced state values (current input values) instead of controller state - // Controller state may be debounced and not updated yet - const amount = railgunAmountFieldValue || railgunWithdrawalAmount - // Prioritize synced state field value, then ENS resolved address, then addressInputState.address + const amount = railgunWithdrawalAmount + // Prioritize ENS resolved address, then synced state field value, then addressInputState.address const address = - railgunAddressStateFieldValue || railgunAddressState.ensAddress || + railgunAddressStateFieldValue || railgunAddressInputState.address // Debug logging console.log('handleRailgunWithdrawal - Input values:', { amount, address, - amountFieldValue: railgunAmountFieldValue, withdrawalAmount: railgunWithdrawalAmount, addressStateFieldValue: railgunAddressStateFieldValue, addressInputStateAddress: railgunAddressInputState.address, @@ -630,7 +633,6 @@ const TransferScreen = () => { token: railgunSelectedToken, amount, address, - amountFieldValue: railgunAmountFieldValue, withdrawalAmount: railgunWithdrawalAmount, addressInputState: railgunAddressInputState.address, addressStateFieldValue: railgunAddressStateFieldValue, @@ -777,7 +779,6 @@ const TransferScreen = () => { }, [ railgunForm, railgunSelectedToken, - railgunAmountFieldValue, railgunWithdrawalAmount, railgunAddressInputState.address, railgunAddressStateFieldValue,