From a0ccbc2a216d68bd11d201e270e944bde3a1e76e Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Mon, 27 Oct 2025 11:38:32 +0530 Subject: [PATCH 01/13] fix: ui fixes --- src/components/TransactionDetails/PerkIcon.tsx | 6 +++++- .../TransactionDetails/TransactionCard.tsx | 13 ++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/TransactionDetails/PerkIcon.tsx b/src/components/TransactionDetails/PerkIcon.tsx index 25343d5a1..ae08a5a9f 100644 --- a/src/components/TransactionDetails/PerkIcon.tsx +++ b/src/components/TransactionDetails/PerkIcon.tsx @@ -1,7 +1,7 @@ import Image from 'next/image' import { STAR_STRAIGHT_ICON } from '@/assets' -type PerkIconSize = 'small' | 'medium' | 'large' +type PerkIconSize = 'extra-small' | 'small' | 'medium' | 'large' interface PerkIconProps { size?: PerkIconSize @@ -9,6 +9,10 @@ interface PerkIconProps { } const sizeConfig = { + 'extra-small': { + container: 'h-8 w-8', + icon: { width: 16, height: 16 }, + }, small: { container: 'h-10 w-10', icon: { width: 22, height: 22 }, diff --git a/src/components/TransactionDetails/TransactionCard.tsx b/src/components/TransactionDetails/TransactionCard.tsx index 83654e410..265ecde49 100644 --- a/src/components/TransactionDetails/TransactionCard.tsx +++ b/src/components/TransactionDetails/TransactionCard.tsx @@ -113,23 +113,19 @@ const TransactionCard: React.FC = ({
{/* txn avatar component handles icon/initials/colors */} - {/*
*/} {isPerkReward ? ( <> - - {status && } + ) : avatarUrl ? ( -
+
Icon - - {status && }
) : ( = ({ isLinkTransaction={isLinkTx} transactionType={type} context="card" - size="small" - // status={status} + size="extra-small" /> )}
From 1fe8934edf4822d64861b8065bdac5e117d177f5 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 28 Oct 2025 13:02:54 +0530 Subject: [PATCH 02/13] fix: decimals and ui fixes --- .../Global/TokenAmountInput/index.tsx | 71 +++++++++++-------- src/components/Payment/PaymentForm/index.tsx | 7 +- .../link/views/Create.request.link.view.tsx | 8 ++- src/utils/general.utils.ts | 17 +++++ tailwind.config.js | 5 ++ 5 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 68fb25d7d..17b239036 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -1,11 +1,12 @@ import { PEANUT_WALLET_TOKEN_DECIMALS, STABLE_COINS } from '@/constants' import { tokenSelectorContext } from '@/context' -import { formatAmountWithoutComma, formatTokenAmount, formatCurrency } from '@/utils' +import { formatAmountWithoutComma, formatTokenAmount, formatCurrency, sanitizeDecimalInput } from '@/utils' import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import Icon from '../Icon' import { twMerge } from 'tailwind-merge' import { Icon as IconComponent } from '@/components/Global/Icons/Icon' import { Slider } from '../Slider' +import { DeviceType, useDeviceType } from '@/hooks/useGetDeviceType' interface TokenAmountInputProps { className?: string @@ -53,6 +54,10 @@ const TokenAmountInput = ({ const { selectedTokenData } = useContext(tokenSelectorContext) const inputRef = useRef(null) const inputType = useMemo(() => (window.innerWidth < 640 ? 'text' : 'number'), []) + const [isFocused, setIsFocused] = useState(false) + const { deviceType } = useDeviceType() + // Only autofocus on desktop (WEB), not on mobile devices (IOS/ANDROID) + const shouldAutoFocus = deviceType === DeviceType.WEB // Store display value for input field (what user sees when typing) const [displayValue, setDisplayValue] = useState(tokenValue || '') @@ -239,33 +244,43 @@ const TokenAmountInput = ({
- {/* Input */} - { - const value = formatAmountWithoutComma(e.target.value) - onChange(value, isInputUsd) - }} - ref={inputRef} - inputMode="decimal" - type={inputType} - value={displayValue} - step="any" - min="0" - autoComplete="off" - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault() - if (onSubmit) onSubmit() - } - }} - onBlur={() => { - if (onBlur) onBlur() - }} - disabled={disabled} - /> + {/* Input with fake caret */} +
+ { + let value = formatAmountWithoutComma(e.target.value) + // Limit to 2 decimal places + value = sanitizeDecimalInput(value, 2) + onChange(value, isInputUsd) + }} + ref={inputRef} + inputMode="decimal" + type={inputType} + value={displayValue} + step="any" + min="0" + autoComplete="off" + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + if (onSubmit) onSubmit() + } + }} + onFocus={() => setIsFocused(true)} + onBlur={() => { + setIsFocused(false) + if (onBlur) onBlur() + }} + disabled={disabled} + /> + {/* Fake blinking caret shown when not focused and input is empty */} + {!isFocused && !displayValue && ( +
+ )} +
{/* Conversion */} diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 48e62ac1c..f9ff7f54b 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -648,9 +648,8 @@ export const PaymentForm = ({ return (
- {!showRequestPotInitialView && ( - - )} + +
{isExternalWalletConnected && isUsingExternalWallet && (
- {showRequestPotInitialView && ( + {showRequestPotInitialView && contributors.length > 0 && (

Contributors ({contributors.length})

{contributors.map((contributor, index) => ( diff --git a/src/components/Request/link/views/Create.request.link.view.tsx b/src/components/Request/link/views/Create.request.link.view.tsx index c2c94083a..57245f27f 100644 --- a/src/components/Request/link/views/Create.request.link.view.tsx +++ b/src/components/Request/link/views/Create.request.link.view.tsx @@ -19,7 +19,7 @@ import { type IToken } from '@/interfaces' import { type IAttachmentOptions } from '@/redux/types/send-flow.types' import { chargesApi } from '@/services/charges' import { requestsApi } from '@/services/requests' -import { fetchTokenSymbol, getRequestLink, isNativeCurrency, printableUsdc } from '@/utils' +import { fetchTokenSymbol, getRequestLink, isNativeCurrency, printableUsdc, sanitizeDecimalInput } from '@/utils' import * as Sentry from '@sentry/nextjs' import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import { useQueryClient } from '@tanstack/react-query' @@ -43,7 +43,11 @@ export const CreateRequestLinkView = () => { const queryClient = useQueryClient() const searchParams = useSearchParams() const paramsAmount = searchParams.get('amount') - const sanitizedAmount = paramsAmount && !isNaN(parseFloat(paramsAmount)) ? paramsAmount : '' + // Sanitize amount and limit to 2 decimal places + const sanitizedAmount = useMemo(() => { + if (!paramsAmount || isNaN(parseFloat(paramsAmount))) return '' + return sanitizeDecimalInput(paramsAmount, 2) + }, [paramsAmount]) const merchant = searchParams.get('merchant') const merchantComment = merchant ? `Bill split for ${merchant}` : null diff --git a/src/utils/general.utils.ts b/src/utils/general.utils.ts index d488f726b..d6cf2b171 100644 --- a/src/utils/general.utils.ts +++ b/src/utils/general.utils.ts @@ -382,6 +382,23 @@ export function formatCurrency(valueStr: string | undefined, maxDecimals: number return formatNumberForDisplay(valueStr, { maxDecimals, minDecimals }) } +/** + * Sanitizes a numeric input string to limit decimal places + * @param value - The input value to sanitize + * @param maxDecimals - Maximum number of decimal places allowed (default: 2) + * @returns Sanitized string with limited decimals + * @example + * sanitizeDecimalInput('123.456', 2) // '123.45' + * sanitizeDecimalInput('123.4', 2) // '123.4' + * sanitizeDecimalInput('123', 2) // '123' + */ +export function sanitizeDecimalInput(value: string, maxDecimals: number = 2): string { + if (!value) return '' + const regex = new RegExp(`^\\d*\\.?\\d{0,${maxDecimals}}`) + const match = value.match(regex) + return match ? match[0] : '' +} + /** * formats a number by: * - displaying 2 significant digits for small numbers (<0.01) diff --git a/tailwind.config.js b/tailwind.config.js index e53e6a824..a53c7e5ff 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -187,6 +187,10 @@ module.exports = { '50%': { opacity: '0.3' }, '100%': { opacity: '1' }, }, + blink: { + '0%, 50%': { opacity: '1' }, + '50.01%, 100%': { opacity: '0' }, + }, }, animation: { colorPulse: 'colorPulse 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite', @@ -194,6 +198,7 @@ module.exports = { pulsate: 'pulsate 1.5s ease-in-out infinite', 'pulsate-slow': 'pulsateDeep 4s ease-in-out infinite', 'pulse-strong': 'pulse-strong 1s ease-in-out infinite', + blink: 'blink 1.5s step-end infinite', }, opacity: { 85: '.85', From 2e054fbeb58dc4a918606358bf562af5effae587 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Tue, 28 Oct 2025 13:56:17 +0530 Subject: [PATCH 03/13] formatting --- src/components/Global/TokenAmountInput/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 17b239036..6e03d748b 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -278,7 +278,7 @@ const TokenAmountInput = ({ /> {/* Fake blinking caret shown when not focused and input is empty */} {!isFocused && !displayValue && ( -
+
)}
From f955c903583e0c407757764289692afed29c0656 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 29 Oct 2025 16:01:19 +0530 Subject: [PATCH 04/13] fix: default slider value --- src/components/Global/Slider/index.tsx | 9 +-- .../Global/TokenAmountInput/index.tsx | 8 ++- src/components/Payment/PaymentForm/index.tsx | 63 +++++++++++++++++-- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/components/Global/Slider/index.tsx b/src/components/Global/Slider/index.tsx index 48346a528..12e539212 100644 --- a/src/components/Global/Slider/index.tsx +++ b/src/components/Global/Slider/index.tsx @@ -15,14 +15,7 @@ function Slider({ ...props }: React.ComponentProps) { // Use internal state for the slider value to enable magnetic snapping - const [internalValue, setInternalValue] = React.useState(controlledValue || defaultValue) - - // Sync with controlled value if it changes externally - React.useEffect(() => { - if (controlledValue) { - setInternalValue(controlledValue) - } - }, [controlledValue]) + const [internalValue, setInternalValue] = React.useState(defaultValue || controlledValue) // Check if current value is at a snap point (exact match) const activeSnapPoint = React.useMemo(() => { diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 6e03d748b..1571371a4 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -30,6 +30,7 @@ interface TokenAmountInputProps { showSlider?: boolean maxAmount?: number isInitialInputUsd?: boolean + defaultSliderValue?: number } const TokenAmountInput = ({ @@ -50,6 +51,7 @@ const TokenAmountInput = ({ showSlider = false, maxAmount, isInitialInputUsd = false, + defaultSliderValue, }: TokenAmountInputProps) => { const { selectedTokenData } = useContext(tokenSelectorContext) const inputRef = useRef(null) @@ -333,7 +335,11 @@ const TokenAmountInput = ({ )} {showSlider && maxAmount && (
- +
)} diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index f9ff7f54b..4e1b348d0 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -22,7 +22,14 @@ import { type ParsedURL } from '@/lib/url-parser/types/payment' import { useAppDispatch, usePaymentStore } from '@/redux/hooks' import { paymentActions } from '@/redux/slices/payment-slice' import { walletActions } from '@/redux/slices/wallet-slice' -import { areEvmAddressesEqual, ErrorHandler, formatAmount, formatCurrency, getContributorsFromCharge } from '@/utils' +import { + areEvmAddressesEqual, + ErrorHandler, + formatAmount, + formatCurrency, + getContributorsFromCharge, + sanitizeDecimalInput, +} from '@/utils' import { useAppKit, useDisconnect } from '@reown/appkit/react' import Image from 'next/image' import { useRouter, useSearchParams } from 'next/navigation' @@ -155,7 +162,7 @@ export const PaymentForm = ({ const isActivePeanutWallet = useMemo(() => !!user && isPeanutWalletConnected, [user, isPeanutWalletConnected]) useEffect(() => { - if (initialSetupDone) return + if (initialSetupDone || showRequestPotInitialView) return if (amount) { setInputTokenAmount(amount) @@ -180,7 +187,7 @@ export const PaymentForm = ({ } setInitialSetupDone(true) - }, [chain, token, amount, initialSetupDone, requestDetails]) + }, [chain, token, amount, initialSetupDone, requestDetails, showRequestPotInitialView]) // reset error when component mounts or recipient changes useEffect(() => { @@ -289,7 +296,7 @@ export const PaymentForm = ({ // fetch token price useEffect(() => { - if (!requestDetails?.tokenAddress || !requestDetails?.chainId) return + if (showRequestPotInitialView || !requestDetails?.tokenAddress || !requestDetails?.chainId) return const getTokenPriceData = async () => { setIsFetchingTokenPrice(true) @@ -317,7 +324,7 @@ export const PaymentForm = ({ } getTokenPriceData() - }, [requestDetails]) + }, [requestDetails, showRequestPotInitialView]) const canInitiatePayment = useMemo(() => { let amountIsSet = false @@ -642,6 +649,51 @@ export const PaymentForm = ({ const totalAmountCollected = requestDetails?.totalCollectedAmount ?? 0 + const defaultSliderPercentage = useMemo(() => { + const charges = requestDetails?.charges + const totalAmount = requestDetails?.tokenAmount ? parseFloat(requestDetails.tokenAmount) : 0 + const totalCollected = totalAmountCollected + + if (totalAmount <= 0) return 0 + + // No charges yet - suggest 100% (full pot) + if (!charges || charges.length === 0) { + return 100 + } + + // Calculate average contribution from existing charges + const contributionAmounts = charges + .map((charge) => parseFloat(charge.tokenAmount)) + .filter((amount) => !isNaN(amount) && amount > 0) + + if (contributionAmounts.length === 0) return 0 + + const avgContribution = contributionAmounts.reduce((sum, amt) => sum + amt, 0) / contributionAmounts.length + + // Calculate remaining amount (could be negative if over-contributed) + const remaining = totalAmount - totalCollected + let suggestedAmount: number + + // If pot is already full or over-filled, suggest minimum contribution + if (remaining <= 0) { + // Pot is full/overfilled - suggest the smallest previous contribution or 10% of pot + const minContribution = Math.min(...contributionAmounts) + suggestedAmount = Math.min(minContribution, totalAmount * 0.1) + } else if (remaining < avgContribution) { + // If remaining is less than average, suggest the remaining amount + suggestedAmount = remaining + } else { + // Otherwise, suggest the average contribution (most common pattern) + suggestedAmount = avgContribution + } + + // Convert amount to percentage of total pot + const percentage = (suggestedAmount / totalAmount) * 100 + setInputTokenAmount(sanitizeDecimalInput(suggestedAmount.toString())) + // Cap at 100% max + return Math.min(percentage, 100) + }, [requestDetails?.charges, requestDetails?.tokenAmount, totalAmountCollected]) + if (fulfillUsingManteca && chargeDetails) { return } @@ -710,6 +762,7 @@ export const PaymentForm = ({ hideBalance={isExternalWalletFlow} showSlider={showRequestPotInitialView && amount ? Number(amount) > 0 : false} maxAmount={showRequestPotInitialView && amount ? Number(amount) : undefined} + defaultSliderValue={defaultSliderPercentage} /> {/* From 6698494ccef317b1717fcc680ea9503b972ea30e Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 29 Oct 2025 16:36:37 +0530 Subject: [PATCH 05/13] add use peanut modal --- src/components/Common/ActionList.tsx | 61 +++++++++++++++++-- src/components/Payment/PaymentForm/index.tsx | 10 +++ src/context/RequestFulfillmentFlowContext.tsx | 6 ++ 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/components/Common/ActionList.tsx b/src/components/Common/ActionList.tsx index 2ed6c897a..352a3e03a 100644 --- a/src/components/Common/ActionList.tsx +++ b/src/components/Common/ActionList.tsx @@ -31,6 +31,7 @@ import { EInviteType } from '@/services/services.types' import ConfirmInviteModal from '../Global/ConfirmInviteModal' import { useGeoLocation } from '@/hooks/useGeoLocation' import Loading from '../Global/Loading' +import { useWallet } from '@/hooks/wallet/useWallet' interface IActionListProps { flow: 'claim' | 'request' @@ -63,6 +64,7 @@ export default function ActionList({ setClaimToMercadoPago, setRegionalMethodType, } = useClaimBankFlow() + const { balance } = useWallet() const [showMinAmountError, setShowMinAmountError] = useState(false) const { claimType } = useDetermineBankClaimType(claimLinkData?.sender?.userId ?? '') const { chargeDetails } = usePaymentStore() @@ -77,16 +79,23 @@ export default function ActionList({ setFlowStep: setRequestFulfilmentBankFlowStep, setFulfillUsingManteca, setRegionalMethodType: setRequestFulfillmentRegionalMethodType, + setTriggerPayWithPeanut, } = useRequestFulfillmentFlow() const [isGuestVerificationModalOpen, setIsGuestVerificationModalOpen] = useState(false) const [selectedMethod, setSelectedMethod] = useState(null) const [showInviteModal, setShowInviteModal] = useState(false) const { user } = useAuth() + const [isUsePeanutBalanceModalShown, setIsUsePeanutBalanceModalShown] = useState(false) + const [showUsePeanutBalanceModal, setShowUsePeanutBalanceModal] = useState(false) const dispatch = useAppDispatch() const { countryCode: userGeoLocationCountryCode, isLoading: isGeoLoading } = useGeoLocation() + // Check if user has enough Peanut balance to pay for the request + const amountInUsd = usdAmount ? parseFloat(usdAmount) : 0 + const hasSufficientPeanutBalance = user && balance && Number(balance) >= amountInUsd + const handleMethodClick = async (method: PaymentMethod) => { if (flow === 'claim' && claimLinkData) { const amountInUsd = parseFloat(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) @@ -124,11 +133,15 @@ export default function ActionList({ break } } else if (flow === 'request' && requestLinkData) { - const amountInUsd = usdAmount ? parseFloat(usdAmount) : 0 if (method.id === 'bank' && amountInUsd < 1) { setShowMinAmountError(true) return } + + if (!isUsePeanutBalanceModalShown && hasSufficientPeanutBalance) { + setShowUsePeanutBalanceModal(true) + return + } switch (method.id) { case 'bank': if (requestType === BankRequestType.GuestKycNeeded) { @@ -243,12 +256,24 @@ export default function ActionList({
{sortedActionMethods.map((method) => { if (flow === 'request' && method.id === 'exchange-or-wallet') { + const shouldShowPeanutBalanceModal = !isUsePeanutBalanceModalShown && hasSufficientPeanutBalance return ( - + onClick={() => { + if (shouldShowPeanutBalanceModal) { + setShowUsePeanutBalanceModal(true) + } + }} + > + {/* Disable daimo pay button if peanut balance is enough to pay for the request */} +
+ +
+
) } @@ -306,6 +331,32 @@ export default function ActionList({ setSelectedMethod(null) }} /> + + { + setShowUsePeanutBalanceModal(false) + setIsUsePeanutBalanceModalShown(true) + }} + title="Use your Peanut balance instead" + description={ + 'You already have enough funds in your Peanut account. Using this method is instant and avoids delays.' + } + icon="user-plus" + ctas={[ + { + text: 'Pay with Peanut', + shadowSize: '4', + onClick: () => { + setShowUsePeanutBalanceModal(false) + setTriggerPayWithPeanut(true) + }, + }, + ]} + iconContainerClassName="bg-primary-1" + preventClose={false} + modalPanelClassName="max-w-md mx-8" + />
) } diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 4e1b348d0..2ca32c6bc 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -93,6 +93,8 @@ export const PaymentForm = ({ setExternalWalletFulfillMethod, fulfillUsingManteca, setFulfillUsingManteca, + triggerPayWithPeanut, + setTriggerPayWithPeanut, } = useRequestFulfillmentFlow() const recipientUsername = !chargeDetails && recipient?.recipientType === 'USERNAME' ? recipient.identifier : null const { user: recipientUser } = useUserByUsername(recipientUsername) @@ -586,6 +588,14 @@ export const PaymentForm = ({ } }, [fulfillUsingManteca, chargeDetails, handleInitiatePayment]) + // Trigger payment with peanut from action list + useEffect(() => { + if (triggerPayWithPeanut) { + handleInitiatePayment() + setTriggerPayWithPeanut(false) + } + }, [triggerPayWithPeanut]) + const isInsufficientBalanceError = useMemo(() => { return error?.includes("You don't have enough balance.") }, [error]) diff --git a/src/context/RequestFulfillmentFlowContext.tsx b/src/context/RequestFulfillmentFlowContext.tsx index d8347f52f..82ee98a12 100644 --- a/src/context/RequestFulfillmentFlowContext.tsx +++ b/src/context/RequestFulfillmentFlowContext.tsx @@ -36,6 +36,8 @@ interface RequestFulfillmentFlowContextType { setFulfillUsingManteca: (fulfillUsingManteca: boolean) => void regionalMethodType: 'mercadopago' | 'pix' setRegionalMethodType: (regionalMethodType: 'mercadopago' | 'pix') => void + triggerPayWithPeanut: boolean + setTriggerPayWithPeanut: (triggerPayWithPeanut: boolean) => void } const RequestFulfillmentFlowContext = createContext(undefined) @@ -53,6 +55,7 @@ export const RequestFulfilmentFlowContextProvider: React.FC<{ children: ReactNod const [requesterDetails, setRequesterDetails] = useState(null) const [fulfillUsingManteca, setFulfillUsingManteca] = useState(false) const [regionalMethodType, setRegionalMethodType] = useState<'mercadopago' | 'pix'>('mercadopago') + const [triggerPayWithPeanut, setTriggerPayWithPeanut] = useState(false) // To trigger the pay with peanut from Action List const resetFlow = useCallback(() => { setExternalWalletFulfillMethod(null) @@ -90,6 +93,8 @@ export const RequestFulfilmentFlowContextProvider: React.FC<{ children: ReactNod setFulfillUsingManteca, regionalMethodType, setRegionalMethodType, + triggerPayWithPeanut, + setTriggerPayWithPeanut, }), [ resetFlow, @@ -103,6 +108,7 @@ export const RequestFulfilmentFlowContextProvider: React.FC<{ children: ReactNod requesterDetails, fulfillUsingManteca, regionalMethodType, + triggerPayWithPeanut, ] ) From 43b57ccc2cafae8081500dc5074adaad5aade9f4 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 29 Oct 2025 16:45:17 +0530 Subject: [PATCH 06/13] cr fixes --- src/components/Global/TokenAmountInput/index.tsx | 8 ++++++-- src/components/TransactionDetails/TransactionCard.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index 1571371a4..c24b5efa2 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { PEANUT_WALLET_TOKEN_DECIMALS, STABLE_COINS } from '@/constants' import { tokenSelectorContext } from '@/context' import { formatAmountWithoutComma, formatTokenAmount, formatCurrency, sanitizeDecimalInput } from '@/utils' @@ -254,8 +256,10 @@ const TokenAmountInput = ({ placeholder={'0.00'} onChange={(e) => { let value = formatAmountWithoutComma(e.target.value) - // Limit to 2 decimal places - value = sanitizeDecimalInput(value, 2) + // USD/currency → 2 decimals; token input → allow `decimals` (<= 6) + const maxDecimals = + displayMode === 'FIAT' || displayMode === 'STABLE' || isInputUsd ? 2 : decimals + value = sanitizeDecimalInput(value, maxDecimals) onChange(value, isInputUsd) }} ref={inputRef} diff --git a/src/components/TransactionDetails/TransactionCard.tsx b/src/components/TransactionDetails/TransactionCard.tsx index 265ecde49..bc95da8e0 100644 --- a/src/components/TransactionDetails/TransactionCard.tsx +++ b/src/components/TransactionDetails/TransactionCard.tsx @@ -122,7 +122,7 @@ const TransactionCard: React.FC = ({ Icon From ff66afe924e9344c51cc9b97cf95d3bb91ac7137 Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 29 Oct 2025 17:25:19 +0530 Subject: [PATCH 07/13] feat(TokenAmountInput): add defaultSliderSuggestedAmount prop and update PaymentForm to utilize it --- src/components/Global/TokenAmountInput/index.tsx | 9 +++++++++ src/components/Payment/PaymentForm/index.tsx | 14 +++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index c24b5efa2..b27eef3d3 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -33,6 +33,7 @@ interface TokenAmountInputProps { maxAmount?: number isInitialInputUsd?: boolean defaultSliderValue?: number + defaultSliderSuggestedAmount?: number } const TokenAmountInput = ({ @@ -54,6 +55,7 @@ const TokenAmountInput = ({ maxAmount, isInitialInputUsd = false, defaultSliderValue, + defaultSliderSuggestedAmount, }: TokenAmountInputProps) => { const { selectedTokenData } = useContext(tokenSelectorContext) const inputRef = useRef(null) @@ -237,6 +239,13 @@ const TokenAmountInput = ({ } } + useEffect(() => { + if (defaultSliderSuggestedAmount) { + setTokenValue(sanitizeDecimalInput(defaultSliderSuggestedAmount.toString(), 2)) + setDisplayValue(sanitizeDecimalInput(defaultSliderSuggestedAmount.toString(), 2)) + } + }, [defaultSliderSuggestedAmount]) + return (
{ + const defaultSliderValue = useMemo(() => { const charges = requestDetails?.charges const totalAmount = requestDetails?.tokenAmount ? parseFloat(requestDetails.tokenAmount) : 0 const totalCollected = totalAmountCollected - if (totalAmount <= 0) return 0 + if (totalAmount <= 0) return { percentage: 0, suggestedAmount: 0 } // No charges yet - suggest 100% (full pot) if (!charges || charges.length === 0) { - return 100 + return { percentage: 100, suggestedAmount: totalAmount } } // Calculate average contribution from existing charges @@ -686,7 +686,7 @@ export const PaymentForm = ({ .map((charge) => parseFloat(charge.tokenAmount)) .filter((amount) => !isNaN(amount) && amount > 0) - if (contributionAmounts.length === 0) return 0 + if (contributionAmounts.length === 0) return { percentage: 0, suggestedAmount: 0 } const avgContribution = contributionAmounts.reduce((sum, amt) => sum + amt, 0) / contributionAmounts.length @@ -709,9 +709,8 @@ export const PaymentForm = ({ // Convert amount to percentage of total pot const percentage = (suggestedAmount / totalAmount) * 100 - setInputTokenAmount(sanitizeDecimalInput(suggestedAmount.toString())) // Cap at 100% max - return Math.min(percentage, 100) + return { percentage: Math.min(percentage, 100), suggestedAmount } }, [requestDetails?.charges, requestDetails?.tokenAmount, totalAmountCollected]) if (fulfillUsingManteca && chargeDetails) { @@ -782,7 +781,8 @@ export const PaymentForm = ({ hideBalance={isExternalWalletFlow} showSlider={showRequestPotInitialView && amount ? Number(amount) > 0 : false} maxAmount={showRequestPotInitialView && amount ? Number(amount) : undefined} - defaultSliderValue={defaultSliderPercentage} + defaultSliderValue={defaultSliderValue.percentage} + defaultSliderSuggestedAmount={defaultSliderValue.suggestedAmount} /> {/* From aba84f524ce43a0dd88dbc4d995753fa25bf913c Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 29 Oct 2025 17:30:58 +0530 Subject: [PATCH 08/13] refactor(PaymentForm): update dependencies in useEffect to include handleInitiatePayment and setTriggerPayWithPeanut --- src/components/Payment/PaymentForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 5be2ac6cb..6f36ffab6 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -604,7 +604,7 @@ export const PaymentForm = ({ handleInitiatePayment() setTriggerPayWithPeanut(false) } - }, [triggerPayWithPeanut]) + }, [triggerPayWithPeanut, handleInitiatePayment, setTriggerPayWithPeanut]) const isInsufficientBalanceError = useMemo(() => { return error?.includes("You don't have enough balance.") From 6c7d1db133f9b84b9f847f7cd27461947b5b15ce Mon Sep 17 00:00:00 2001 From: Zishan Mohd Date: Wed, 29 Oct 2025 21:23:43 +0530 Subject: [PATCH 09/13] remove sanitizeDecimal utility and improve formatTokenAmount to handle imput fields --- .../Global/TokenAmountInput/index.tsx | 18 ++++--- src/components/Payment/PaymentForm/index.tsx | 10 +--- .../link/views/Create.request.link.view.tsx | 7 ++- src/utils/general.utils.ts | 54 +++++++++---------- 4 files changed, 42 insertions(+), 47 deletions(-) diff --git a/src/components/Global/TokenAmountInput/index.tsx b/src/components/Global/TokenAmountInput/index.tsx index b27eef3d3..c53233a9b 100644 --- a/src/components/Global/TokenAmountInput/index.tsx +++ b/src/components/Global/TokenAmountInput/index.tsx @@ -2,7 +2,7 @@ import { PEANUT_WALLET_TOKEN_DECIMALS, STABLE_COINS } from '@/constants' import { tokenSelectorContext } from '@/context' -import { formatAmountWithoutComma, formatTokenAmount, formatCurrency, sanitizeDecimalInput } from '@/utils' +import { formatTokenAmount, formatCurrency } from '@/utils' import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import Icon from '../Icon' import { twMerge } from 'tailwind-merge' @@ -241,8 +241,11 @@ const TokenAmountInput = ({ useEffect(() => { if (defaultSliderSuggestedAmount) { - setTokenValue(sanitizeDecimalInput(defaultSliderSuggestedAmount.toString(), 2)) - setDisplayValue(sanitizeDecimalInput(defaultSliderSuggestedAmount.toString(), 2)) + const formattedAmount = formatTokenAmount(defaultSliderSuggestedAmount.toString(), 2) + if (formattedAmount) { + setTokenValue(formattedAmount) + setDisplayValue(formattedAmount) + } } }, [defaultSliderSuggestedAmount]) @@ -264,11 +267,14 @@ const TokenAmountInput = ({ className={`h-12 w-[4ch] max-w-80 bg-transparent text-6xl font-black caret-primary-1 outline-none transition-colors placeholder:text-h1 placeholder:text-gray-1 focus:border-primary-1 dark:border-white dark:bg-n-1 dark:text-white dark:placeholder:text-white/75 dark:focus:border-primary-1`} placeholder={'0.00'} onChange={(e) => { - let value = formatAmountWithoutComma(e.target.value) + let value = e.target.value // USD/currency → 2 decimals; token input → allow `decimals` (<= 6) const maxDecimals = displayMode === 'FIAT' || displayMode === 'STABLE' || isInputUsd ? 2 : decimals - value = sanitizeDecimalInput(value, maxDecimals) + const formattedAmount = formatTokenAmount(value, maxDecimals, true) + if (formattedAmount !== undefined) { + value = formattedAmount + } onChange(value, isInputUsd) }} ref={inputRef} @@ -293,7 +299,7 @@ const TokenAmountInput = ({ /> {/* Fake blinking caret shown when not focused and input is empty */} {!isFocused && !displayValue && ( -
+
)}
diff --git a/src/components/Payment/PaymentForm/index.tsx b/src/components/Payment/PaymentForm/index.tsx index 6f36ffab6..49061d39c 100644 --- a/src/components/Payment/PaymentForm/index.tsx +++ b/src/components/Payment/PaymentForm/index.tsx @@ -24,14 +24,7 @@ import { type ParsedURL } from '@/lib/url-parser/types/payment' import { useAppDispatch, usePaymentStore } from '@/redux/hooks' import { paymentActions } from '@/redux/slices/payment-slice' import { walletActions } from '@/redux/slices/wallet-slice' -import { - areEvmAddressesEqual, - ErrorHandler, - formatAmount, - formatCurrency, - getContributorsFromCharge, - sanitizeDecimalInput, -} from '@/utils' +import { areEvmAddressesEqual, ErrorHandler, formatAmount, formatCurrency, getContributorsFromCharge } from '@/utils' import { useAppKit, useDisconnect } from '@reown/appkit/react' import Image from 'next/image' import { useRouter, useSearchParams } from 'next/navigation' @@ -720,7 +713,6 @@ export const PaymentForm = ({ return (
-
{isExternalWalletConnected && isUsingExternalWallet && (