From 9bfaeab5dac9447ea3caa371e8c7b548849155d3 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:26:07 +0530 Subject: [PATCH 1/9] feat: LimitsWarningCard component for displaying limit warnings and errors --- src/components/Global/Icons/Icon.tsx | 3 + .../limits/components/LimitsWarningCard.tsx | 85 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/features/limits/components/LimitsWarningCard.tsx diff --git a/src/components/Global/Icons/Icon.tsx b/src/components/Global/Icons/Icon.tsx index 842ebb9cc..8760cf18a 100644 --- a/src/components/Global/Icons/Icon.tsx +++ b/src/components/Global/Icons/Icon.tsx @@ -63,6 +63,7 @@ import { CompareArrowsRounded, WarningRounded, SpeedRounded, + InfoRounded, } from '@mui/icons-material' import { DocsIcon } from './docs' import { PeanutSupportIcon } from './peanut-support' @@ -122,6 +123,7 @@ export type IconName = | 'error' | 'clip' | 'info' + | 'info-filled' | 'external-link' | 'plus' | 'switch' @@ -254,6 +256,7 @@ const iconComponents: Record>> = clip: (props) => , info: (props) => , 'external-link': (props) => , + 'info-filled': (props) => , plus: (props) => , alert: (props) => , switch: (props) => , diff --git a/src/features/limits/components/LimitsWarningCard.tsx b/src/features/limits/components/LimitsWarningCard.tsx new file mode 100644 index 000000000..c0004fa31 --- /dev/null +++ b/src/features/limits/components/LimitsWarningCard.tsx @@ -0,0 +1,85 @@ +'use client' + +import InfoCard from '@/components/Global/InfoCard' +import { Icon, type IconProps } from '@/components/Global/Icons/Icon' +import Link from 'next/link' +import { useModalsContext } from '@/context/ModalsContext' +import { twMerge } from 'tailwind-merge' + +export type LimitsWarningType = 'warning' | 'error' + +interface LimitsWarningItem { + text: string + isLink?: boolean + href?: string + icon?: IconProps['name'] +} + +interface LimitsWarningCardProps { + type: LimitsWarningType + title: string + items: LimitsWarningItem[] + showSupportLink?: boolean + className?: string +} + +const SUPPORT_MESSAGE = 'Hi, I would like to increase my payment limits.' + +/** + * reusable card for displaying limit warnings (yellow) or blocking errors (red) + * used across qr payments, add money, and withdraw flows + */ +export default function LimitsWarningCard({ + type, + title, + items, + showSupportLink = true, + className, +}: LimitsWarningCardProps) { + const { openSupportWithMessage } = useModalsContext() + + return ( + +
    + {items.map((item, index) => ( +
  • + {item.isLink && item.href ? ( + + {item.icon && ( + + )} + {item.text} + + ) : ( + item.text + )} +
  • + ))} +
+ {showSupportLink && ( + <> +
+ + + )} +
+ } + /> + ) +} From f35d994cb445781bc8127fb567257146585d6132 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:26:48 +0530 Subject: [PATCH 2/9] feat: add manteca withdraw limits and useLimitsValidation hook for txn limit validation --- src/constants/payment.consts.ts | 4 + .../limits/hooks/useLimitsValidation.ts | 279 ++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 src/features/limits/hooks/useLimitsValidation.ts diff --git a/src/constants/payment.consts.ts b/src/constants/payment.consts.ts index 9d51ef5b9..9c3914a04 100644 --- a/src/constants/payment.consts.ts +++ b/src/constants/payment.consts.ts @@ -7,6 +7,10 @@ export const MIN_PIX_AMOUNT = 5 export const MAX_MANTECA_DEPOSIT_AMOUNT = 2000 export const MIN_MANTECA_DEPOSIT_AMOUNT = 1 +// withdraw limits for manteca regional offramps (in USD) +export const MAX_MANTECA_WITHDRAW_AMOUNT = 2000 +export const MIN_MANTECA_WITHDRAW_AMOUNT = 1 + // QR payment limits for manteca (PIX, MercadoPago, QR3) export const MIN_MANTECA_QR_PAYMENT_AMOUNT = 0.1 // Manteca provider minimum export const MAX_QR_PAYMENT_AMOUNT_FOREIGN = 2000 // max per transaction for foreign users diff --git a/src/features/limits/hooks/useLimitsValidation.ts b/src/features/limits/hooks/useLimitsValidation.ts new file mode 100644 index 000000000..7f16d1c9f --- /dev/null +++ b/src/features/limits/hooks/useLimitsValidation.ts @@ -0,0 +1,279 @@ +'use client' + +import { useMemo } from 'react' +import { useLimits } from '@/hooks/useLimits' +import useKycStatus from '@/hooks/useKycStatus' +import type { MantecaLimit } from '@/interfaces' +import { + MAX_QR_PAYMENT_AMOUNT_FOREIGN, + MAX_MANTECA_DEPOSIT_AMOUNT, + MAX_MANTECA_WITHDRAW_AMOUNT, +} from '@/constants/payment.consts' +import { formatExtendedNumber } from '@/utils/general.utils' + +// threshold for showing warning (percentage of limit remaining after transaction) +const WARNING_THRESHOLD_PERCENT = 30 + +export type LimitValidationResult = { + isBlocking: boolean + isWarning: boolean + remainingLimit: number | null + totalLimit: number | null + message: string | null + daysUntilReset: number | null +} + +export type LimitFlowType = 'onramp' | 'offramp' | 'qr-payment' +export type LimitCurrency = 'ARS' | 'BRL' | 'USD' + +interface UseLimitsValidationOptions { + flowType: LimitFlowType + amount: number | string | null | undefined + currency?: LimitCurrency + // for qr payments, whether user is local (arg/brazil) or foreign + isLocalUser?: boolean +} + +/** + * hook to validate amounts against user's transaction limits + * returns warning/blocking state based on remaining limits + */ +export function useLimitsValidation({ + flowType, + amount, + currency = 'USD', + isLocalUser = false, +}: UseLimitsValidationOptions) { + const { mantecaLimits, bridgeLimits, isLoading, hasMantecaLimits, hasBridgeLimits } = useLimits() + const { isUserMantecaKycApproved, isUserBridgeKycApproved } = useKycStatus() + + // parse amount to number + const numericAmount = useMemo(() => { + if (!amount) return 0 + const parsed = typeof amount === 'string' ? parseFloat(amount) : amount + return isNaN(parsed) ? 0 : parsed + }, [amount]) + + // get relevant manteca limit based on currency + const relevantMantecaLimit = useMemo(() => { + if (!mantecaLimits || mantecaLimits.length === 0) return null + return mantecaLimits.find((limit) => limit.asset === currency) ?? null + }, [mantecaLimits, currency]) + + // calculate days until monthly reset (first of next month) + const daysUntilMonthlyReset = useMemo(() => { + const now = new Date() + const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1) + const diffTime = nextMonth.getTime() - now.getTime() + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + }, []) + + // validate for manteca users (argentina/brazil) + const mantecaValidation = useMemo(() => { + if (!isUserMantecaKycApproved || !relevantMantecaLimit) { + return { + isBlocking: false, + isWarning: false, + remainingLimit: null, + totalLimit: null, + message: null, + daysUntilReset: null, + } + } + + const monthlyLimit = parseFloat(relevantMantecaLimit.monthlyLimit) + const availableMonthly = parseFloat(relevantMantecaLimit.availableMonthlyLimit) + + // per-transaction max for manteca (different for onramp vs offramp) + const perTxMax = flowType === 'onramp' ? MAX_MANTECA_DEPOSIT_AMOUNT : MAX_MANTECA_WITHDRAW_AMOUNT + + // effective limit is the lower of per-tx max and available monthly + const effectiveLimit = Math.min(perTxMax, availableMonthly) + + // check if amount exceeds per-transaction max first (more restrictive) + if (numericAmount > perTxMax) { + return { + isBlocking: true, + isWarning: false, + remainingLimit: perTxMax, + totalLimit: perTxMax, + message: `Maximum ${flowType === 'onramp' ? 'deposit' : 'withdrawal'} is $${formatExtendedNumber(perTxMax)} per transaction`, + daysUntilReset: null, // per-tx limit doesn't reset + } + } + + // check if amount exceeds remaining monthly limit + if (numericAmount > availableMonthly) { + return { + isBlocking: true, + isWarning: false, + remainingLimit: availableMonthly, + totalLimit: monthlyLimit, + message: `Amount exceeds your remaining limit of ${formatExtendedNumber(availableMonthly)} ${currency}`, + daysUntilReset: daysUntilMonthlyReset, + } + } + + // check if amount is close to limit (warning) + const afterTransaction = availableMonthly - numericAmount + const afterPercent = monthlyLimit > 0 ? (afterTransaction / monthlyLimit) * 100 : 0 + + if (afterPercent < WARNING_THRESHOLD_PERCENT && numericAmount > 0) { + return { + isBlocking: false, + isWarning: true, + remainingLimit: effectiveLimit, + totalLimit: monthlyLimit, + message: `This transaction will use most of your remaining limit`, + daysUntilReset: daysUntilMonthlyReset, + } + } + + return { + isBlocking: false, + isWarning: false, + remainingLimit: effectiveLimit, + totalLimit: monthlyLimit, + message: null, + daysUntilReset: daysUntilMonthlyReset, + } + }, [isUserMantecaKycApproved, relevantMantecaLimit, numericAmount, currency, daysUntilMonthlyReset, flowType]) + + // validate for bridge users (us/europe/mexico) - per transaction limits + const bridgeValidation = useMemo(() => { + if (!isUserBridgeKycApproved || !bridgeLimits) { + return { + isBlocking: false, + isWarning: false, + remainingLimit: null, + totalLimit: null, + message: null, + daysUntilReset: null, + } + } + + // bridge has per-transaction limits, not cumulative + const perTxLimit = + flowType === 'onramp' + ? parseFloat(bridgeLimits.onRampPerTransaction) + : parseFloat(bridgeLimits.offRampPerTransaction) + + if (numericAmount > perTxLimit) { + return { + isBlocking: true, + isWarning: false, + remainingLimit: perTxLimit, + totalLimit: perTxLimit, + message: `Amount exceeds per-transaction limit of $${formatExtendedNumber(perTxLimit)}`, + daysUntilReset: null, + } + } + + // warning when close to per-tx limit + const usagePercent = perTxLimit > 0 ? (numericAmount / perTxLimit) * 100 : 0 + if (usagePercent > 80 && numericAmount > 0) { + return { + isBlocking: false, + isWarning: true, + remainingLimit: perTxLimit, + totalLimit: perTxLimit, + message: `This amount is close to your per-transaction limit`, + daysUntilReset: null, + } + } + + return { + isBlocking: false, + isWarning: false, + remainingLimit: perTxLimit, + totalLimit: perTxLimit, + message: null, + daysUntilReset: null, + } + }, [isUserBridgeKycApproved, bridgeLimits, flowType, numericAmount]) + + // qr payment validation for foreign users (non-manteca kyc) + const foreignQrValidation = useMemo(() => { + if (flowType !== 'qr-payment' || isLocalUser) { + return { + isBlocking: false, + isWarning: false, + remainingLimit: null, + totalLimit: null, + message: null, + daysUntilReset: null, + } + } + + // foreign users have a per-transaction limit for qr payments + if (numericAmount > MAX_QR_PAYMENT_AMOUNT_FOREIGN) { + return { + isBlocking: true, + isWarning: false, + remainingLimit: MAX_QR_PAYMENT_AMOUNT_FOREIGN, + totalLimit: MAX_QR_PAYMENT_AMOUNT_FOREIGN, + message: `QR payment limit is $${MAX_QR_PAYMENT_AMOUNT_FOREIGN.toLocaleString()} per transaction`, + daysUntilReset: null, + } + } + + return { + isBlocking: false, + isWarning: false, + remainingLimit: MAX_QR_PAYMENT_AMOUNT_FOREIGN, + totalLimit: MAX_QR_PAYMENT_AMOUNT_FOREIGN, + message: null, + daysUntilReset: null, + } + }, [flowType, isLocalUser, numericAmount]) + + // combined result - prioritize manteca for local users, bridge for others + const validation = useMemo(() => { + // for qr payments + if (flowType === 'qr-payment') { + // local users (manteca kyc) use manteca limits + if (isLocalUser && isUserMantecaKycApproved) { + return mantecaValidation + } + // foreign users have fixed per-tx limit + return foreignQrValidation + } + + // for onramp/offramp - check which provider applies + if (isUserMantecaKycApproved && hasMantecaLimits) { + return mantecaValidation + } + if (isUserBridgeKycApproved && hasBridgeLimits) { + return bridgeValidation + } + + // no kyc - no limits to validate + return { + isBlocking: false, + isWarning: false, + remainingLimit: null, + totalLimit: null, + message: null, + daysUntilReset: null, + } + }, [ + flowType, + isLocalUser, + isUserMantecaKycApproved, + isUserBridgeKycApproved, + hasMantecaLimits, + hasBridgeLimits, + mantecaValidation, + bridgeValidation, + foreignQrValidation, + ]) + + return { + ...validation, + isLoading, + // convenience getters + hasLimits: hasMantecaLimits || hasBridgeLimits, + isMantecaUser: isUserMantecaKycApproved, + isBridgeUser: isUserBridgeKycApproved, + } +} From 3bf65176e67bf6ed89e5f60d180cf3b1ae848274 Mon Sep 17 00:00:00 2001 From: kushagrasarathe <76868364+kushagrasarathe@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:27:20 +0530 Subject: [PATCH 3/9] feat: implement limits validation on payment flows --- .../add-money/[country]/bank/page.tsx | 40 ++++++++++++- src/app/(mobile-ui)/qr-pay/page.tsx | 58 +++++++++++++++++- src/app/(mobile-ui)/withdraw/manteca/page.tsx | 59 ++++++++++++++++--- src/app/(mobile-ui)/withdraw/page.tsx | 49 ++++++++++++++- .../AddMoney/components/InputAmountStep.tsx | 43 +++++++++++++- .../AddMoney/components/MantecaAddMoney.tsx | 24 ++++++-- 6 files changed, 251 insertions(+), 22 deletions(-) diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index 8ce1ff0e2..0ce1adfe7 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -26,6 +26,9 @@ import { OnrampConfirmationModal } from '@/components/AddMoney/components/Onramp import { InitiateBridgeKYCModal } from '@/components/Kyc/InitiateBridgeKYCModal' import InfoCard from '@/components/Global/InfoCard' import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs' +import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation' +import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' +import { formatExtendedNumber } from '@/utils/general.utils' // Step type for URL state type BridgeBankStep = 'inputAmount' | 'kyc' | 'collectUserDetails' | 'showDetails' @@ -97,6 +100,14 @@ export default function OnrampBankPage() { return getMinimumAmount(selectedCountry.id) }, [selectedCountry?.id]) + // validate against user's bridge limits + const limitsValidation = useLimitsValidation({ + flowType: 'onramp', + amount: rawTokenAmount, + currency: 'USD', + isLocalUser: false, // bridge is for non-local users + }) + // Determine initial step based on KYC status (only when URL has no step) useEffect(() => { // If URL already has a step, respect it (allows deep linking) @@ -341,6 +352,8 @@ export default function OnrampBankPage() { } if (urlState.step === 'inputAmount') { + const showLimitsCard = limitsValidation.isBlocking || limitsValidation.isWarning + return (
@@ -364,6 +377,25 @@ export default function OnrampBankPage() { hideBalance /> + {/* limits warning/error card */} + {showLimitsCard && ( + + )} + Continue - {error.showError && !!error.errorMessage && } + {/* only show error if limits card is not displayed */} + {error.showError && !!error.errorMessage && !showLimitsCard && ( + + )}
('none') @@ -383,6 +389,26 @@ export default function QRPayPage() { } }, [paymentProcessor, simpleFiPayment, paymentLock?.code, paymentLock?.paymentAgainstAmount, amount]) + // determine if user is local (manteca kyc) for limits validation + const isLocalUser = useMemo(() => { + return isUserMantecaKycApproved && paymentProcessor === 'MANTECA' + }, [isUserMantecaKycApproved, paymentProcessor]) + + // determine currency for limits validation based on qr type + const limitsCurrency = useMemo(() => { + if (qrType === EQrType.PIX) return 'BRL' as const + if (qrType === EQrType.MERCADO_PAGO || qrType === EQrType.ARGENTINA_QR3) return 'ARS' as const + return 'USD' as const + }, [qrType]) + + // validate payment against user's limits + const limitsValidation = useLimitsValidation({ + flowType: 'qr-payment', + amount: usdAmount, + currency: limitsCurrency, + isLocalUser, + }) + // Fetch points early to avoid latency penalty - fetch as soon as we have usdAmount // This way points are cached by the time success view shows // Only Manteca QR payments give points (SimpleFi does not) @@ -1535,7 +1561,34 @@ export default function QRPayPage() { hideBalance /> )} - {balanceErrorMessage && } + {/* only show balance error if limits card is not displayed */} + {balanceErrorMessage && !limitsValidation.isBlocking && !limitsValidation.isWarning && ( + + )} + + {/* Limits Warning/Error Card */} + {(limitsValidation.isBlocking || limitsValidation.isWarning) && ( + + )} {/* Information Card */} @@ -1560,7 +1613,8 @@ export default function QRPayPage() { shouldBlockPay || !usdAmount || usdAmount === '0.00' || - isWaitingForWebSocket + isWaitingForWebSocket || + limitsValidation.isBlocking } > {isLoading || isWaitingForWebSocket diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index 1436a8f7f..bd3b861bf 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -45,12 +45,13 @@ import { } from '@/constants/manteca.consts' import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { TRANSACTIONS } from '@/constants/query.consts' +import { useLimitsValidation, type LimitCurrency } from '@/features/limits/hooks/useLimitsValidation' +import { MIN_MANTECA_WITHDRAW_AMOUNT } from '@/constants/payment.consts' +import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' +import { formatExtendedNumber } from '@/utils/general.utils' type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' | 'failure' -const MAX_WITHDRAW_AMOUNT = '2000' -const MIN_WITHDRAW_AMOUNT = '1' - export default function MantecaWithdrawFlow() { const flowId = useId() // Unique ID per flow instance to prevent cache collisions const [currencyAmount, setCurrencyAmount] = useState(undefined) @@ -102,6 +103,21 @@ export default function MantecaWithdrawFlow() { // Initialize KYC flow hook const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry }) + // determine currency for limits validation + const limitsCurrency = useMemo(() => { + const currency = selectedCountry?.currency?.toUpperCase() + if (currency === 'ARS' || currency === 'BRL') return currency as LimitCurrency + return 'USD' + }, [selectedCountry?.currency]) + + // validate against user's limits + const limitsValidation = useLimitsValidation({ + flowType: 'offramp', + amount: usdAmount, + currency: limitsCurrency, + isLocalUser: true, // manteca is for local users + }) + // WebSocket listener for KYC status updates useWebSocket({ username: user?.user.username ?? undefined, @@ -290,10 +306,9 @@ export default function MantecaWithdrawFlow() { return } const paymentAmount = parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS) - if (paymentAmount < parseUnits(MIN_WITHDRAW_AMOUNT, PEANUT_WALLET_TOKEN_DECIMALS)) { - setBalanceErrorMessage(`Withdraw amount must be at least $${MIN_WITHDRAW_AMOUNT}`) - } else if (paymentAmount > parseUnits(MAX_WITHDRAW_AMOUNT, PEANUT_WALLET_TOKEN_DECIMALS)) { - setBalanceErrorMessage(`Withdraw amount exceeds maximum limit of $${MAX_WITHDRAW_AMOUNT}`) + // only check min amount and balance here - max amount is handled by limits validation + if (paymentAmount < parseUnits(MIN_MANTECA_WITHDRAW_AMOUNT.toString(), PEANUT_WALLET_TOKEN_DECIMALS)) { + setBalanceErrorMessage(`Withdraw amount must be at least $${MIN_MANTECA_WITHDRAW_AMOUNT}`) } else if (paymentAmount > balance) { setBalanceErrorMessage('Not enough balance to complete withdrawal.') } else { @@ -429,6 +444,29 @@ export default function MantecaWithdrawFlow() { balance ? formatAmount(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) : undefined } /> + + {/* limits warning/error card */} + {(limitsValidation.isBlocking || limitsValidation.isWarning) && ( + + )} + - {balanceErrorMessage && } + {/* only show balance error if limits card is not displayed */} + {balanceErrorMessage && !limitsValidation.isBlocking && !limitsValidation.isWarning && ( + + )} )} diff --git a/src/app/(mobile-ui)/withdraw/page.tsx b/src/app/(mobile-ui)/withdraw/page.tsx index 19cc8feb8..01cde8224 100644 --- a/src/app/(mobile-ui)/withdraw/page.tsx +++ b/src/app/(mobile-ui)/withdraw/page.tsx @@ -9,11 +9,13 @@ import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { useWithdrawFlow } from '@/context/WithdrawFlowContext' import { useWallet } from '@/hooks/wallet/useWallet' import { tokenSelectorContext } from '@/context/tokenSelector.context' -import { formatAmount } from '@/utils/general.utils' +import { formatAmount, formatExtendedNumber } from '@/utils/general.utils' import { getCountryFromAccount } from '@/utils/bridge.utils' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useMemo, useState, useRef, useContext } from 'react' import { formatUnits } from 'viem' +import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation' +import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' type WithdrawStep = 'inputAmount' | 'selectMethod' @@ -79,6 +81,15 @@ export default function WithdrawPage() { return balance !== undefined ? formatAmount(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) : '' }, [balance]) + // validate against user's limits for bank withdrawals + // note: crypto withdrawals don't have fiat limits + const limitsValidation = useLimitsValidation({ + flowType: 'offramp', + amount: rawTokenAmount, + currency: 'USD', + isLocalUser: selectedMethod?.type === 'manteca', + }) + // clear errors and reset any persisted state when component mounts to ensure clean state useEffect(() => { setError({ showError: false, errorMessage: '' }) @@ -247,6 +258,10 @@ export default function WithdrawPage() { }, [rawTokenAmount, maxDecimalAmount, error.showError, selectedTokenData?.price]) if (step === 'inputAmount') { + // only show limits card for bank/manteca withdrawals, not crypto + const showLimitsCard = + selectedMethod?.type !== 'crypto' && (limitsValidation.isBlocking || limitsValidation.isWarning) + return (
+ + {/* limits warning/error card for bank withdrawals */} + {showLimitsCard && ( + + )} + - {error.showError && !!error.errorMessage && } + {/* only show error if limits card is not displayed */} + {error.showError && !!error.errorMessage && !showLimitsCard && ( + + )}
) diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx index e58cec3bc..4a387ec7a 100644 --- a/src/components/AddMoney/components/InputAmountStep.tsx +++ b/src/components/AddMoney/components/InputAmountStep.tsx @@ -8,8 +8,18 @@ import { useRouter } from 'next/navigation' import ErrorAlert from '@/components/Global/ErrorAlert' import { useCurrency } from '@/hooks/useCurrency' import PeanutLoading from '@/components/Global/PeanutLoading' +import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' +import { formatExtendedNumber } from '@/utils/general.utils' type ICurrency = ReturnType + +interface LimitsValidationProps { + isBlocking: boolean + isWarning: boolean + remainingLimit: number | null + daysUntilReset: number | null +} + interface InputAmountStepProps { onSubmit: () => void isLoading: boolean @@ -21,6 +31,8 @@ interface InputAmountStepProps { setCurrentDenomination?: (denomination: string) => void initialDenomination?: string setDisplayedAmount?: (value: string) => void + limitsValidation?: LimitsValidationProps + limitsCurrency?: string } const InputAmountStep = ({ @@ -34,6 +46,8 @@ const InputAmountStep = ({ setCurrentDenomination, initialDenomination, setDisplayedAmount, + limitsValidation, + limitsCurrency = 'USD', }: InputAmountStepProps) => { const router = useRouter() @@ -41,6 +55,8 @@ const InputAmountStep = ({ return } + const showLimitsCard = limitsValidation?.isBlocking || limitsValidation?.isWarning + return (
router.back()} /> @@ -66,6 +82,28 @@ const InputAmountStep = ({ setCurrentDenomination={setCurrentDenomination} hideBalance /> + + {/* limits warning/error card */} + {showLimitsCard && ( + + )} +
This must exactly match what you send from your bank @@ -74,13 +112,14 @@ const InputAmountStep = ({ variant="purple" shadowSize="4" onClick={onSubmit} - disabled={!!error || isLoading || !parseFloat(tokenAmount)} + disabled={!!error || isLoading || !parseFloat(tokenAmount) || limitsValidation?.isBlocking} className="w-full" loading={isLoading} > Continue - {error && } + {/* only show error if limits card is not displayed */} + {error && !showLimitsCard && }
) diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx index a3bada947..7c03eb8ec 100644 --- a/src/components/AddMoney/components/MantecaAddMoney.tsx +++ b/src/components/AddMoney/components/MantecaAddMoney.tsx @@ -14,10 +14,11 @@ import { mantecaApi } from '@/services/manteca' import { parseUnits } from 'viem' import { useQueryClient } from '@tanstack/react-query' import useKycStatus from '@/hooks/useKycStatus' -import { MAX_MANTECA_DEPOSIT_AMOUNT, MIN_MANTECA_DEPOSIT_AMOUNT } from '@/constants/payment.consts' +import { MIN_MANTECA_DEPOSIT_AMOUNT } from '@/constants/payment.consts' import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { TRANSACTIONS } from '@/constants/query.consts' import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs' +import { useLimitsValidation, type LimitCurrency } from '@/features/limits/hooks/useLimitsValidation' // Step type for URL state type MantecaStep = 'inputAmount' | 'depositDetails' @@ -67,6 +68,21 @@ const MantecaAddMoney: FC = () => { const currencyData = useCurrency(selectedCountry?.currency ?? 'ARS') const { user, fetchUser } = useAuth() + // determine currency for limits validation + const limitsCurrency = useMemo(() => { + const currency = selectedCountry?.currency?.toUpperCase() + if (currency === 'ARS' || currency === 'BRL') return currency as LimitCurrency + return 'USD' + }, [selectedCountry?.currency]) + + // validate against user's limits + const limitsValidation = useLimitsValidation({ + flowType: 'onramp', + amount: usdAmount, + currency: limitsCurrency, + isLocalUser: true, // manteca is for local users + }) + useWebSocket({ username: user?.user.username ?? undefined, autoConnect: !!user?.user.username, @@ -79,7 +95,7 @@ const MantecaAddMoney: FC = () => { }, }) - // Validate USD amount (for min/max checks which are in USD) + // Validate USD amount (min check only - max is handled by limits validation) useEffect(() => { if (!usdAmount || usdAmount === '0.00') { setError(null) @@ -88,8 +104,6 @@ const MantecaAddMoney: FC = () => { const paymentAmount = parseUnits(usdAmount, PEANUT_WALLET_TOKEN_DECIMALS) if (paymentAmount < parseUnits(MIN_MANTECA_DEPOSIT_AMOUNT.toString(), PEANUT_WALLET_TOKEN_DECIMALS)) { setError(`Deposit amount must be at least $${MIN_MANTECA_DEPOSIT_AMOUNT}`) - } else if (paymentAmount > parseUnits(MAX_MANTECA_DEPOSIT_AMOUNT.toString(), PEANUT_WALLET_TOKEN_DECIMALS)) { - setError(`Deposit amount exceeds maximum limit of $${MAX_MANTECA_DEPOSIT_AMOUNT}`) } else { setError(null) } @@ -207,6 +221,8 @@ const MantecaAddMoney: FC = () => { setCurrentDenomination={handleDenominationChange} initialDenomination={currentDenomination} setDisplayedAmount={handleDisplayedAmountChange} + limitsValidation={limitsValidation} + limitsCurrency={limitsCurrency} /> {isKycModalOpen && ( Date: Thu, 15 Jan 2026 16:56:48 +0530 Subject: [PATCH 4/9] fix: cr dry suggestion --- .../add-money/[country]/bank/page.tsx | 18 +++++++------ src/app/(mobile-ui)/qr-pay/page.tsx | 18 +++++++------ src/app/(mobile-ui)/withdraw/manteca/page.tsx | 26 +++++++++---------- src/app/(mobile-ui)/withdraw/page.tsx | 18 +++++++------ .../AddMoney/components/InputAmountStep.tsx | 13 ++++------ .../AddMoney/components/MantecaAddMoney.tsx | 9 +++---- .../limits/components/LimitsWarningCard.tsx | 5 ++-- src/features/limits/utils/limits.utils.ts | 19 ++++++++++++++ 8 files changed, 73 insertions(+), 53 deletions(-) diff --git a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx index 0ce1adfe7..d1e3f3ced 100644 --- a/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx +++ b/src/app/(mobile-ui)/add-money/[country]/bank/page.tsx @@ -29,6 +29,7 @@ import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs' import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation' import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' import { formatExtendedNumber } from '@/utils/general.utils' +import { LIMITS_COPY } from '@/features/limits/utils/limits.utils' // Step type for URL state type BridgeBankStep = 'inputAmount' | 'kyc' | 'collectUserDetails' | 'showDetails' @@ -381,16 +382,17 @@ export default function OnrampBankPage() { {showLimitsCard && ( @@ -417,8 +419,8 @@ export default function OnrampBankPage() { > Continue - {/* only show error if limits card is not displayed */} - {error.showError && !!error.errorMessage && !showLimitsCard && ( + {/* only show error if limits blocking card is not displayed (warnings can coexist) */} + {error.showError && !!error.errorMessage && !limitsValidation.isBlocking && ( )} diff --git a/src/app/(mobile-ui)/qr-pay/page.tsx b/src/app/(mobile-ui)/qr-pay/page.tsx index 14fc68683..41b4280af 100644 --- a/src/app/(mobile-ui)/qr-pay/page.tsx +++ b/src/app/(mobile-ui)/qr-pay/page.tsx @@ -71,6 +71,7 @@ import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' import useKycStatus from '@/hooks/useKycStatus' import { MAX_QR_PAYMENT_AMOUNT_FOREIGN } from '@/constants/payment.consts' import { formatExtendedNumber } from '@/utils/general.utils' +import { LIMITS_COPY } from '@/features/limits/utils/limits.utils' const MAX_QR_PAYMENT_AMOUNT = '2000' const MIN_QR_PAYMENT_AMOUNT = '0.1' @@ -1561,8 +1562,8 @@ export default function QRPayPage() { hideBalance /> )} - {/* only show balance error if limits card is not displayed */} - {balanceErrorMessage && !limitsValidation.isBlocking && !limitsValidation.isWarning && ( + {/* only show balance error if limits blocking card is not displayed (warnings can coexist) */} + {balanceErrorMessage && !limitsValidation.isBlocking && ( )} @@ -1570,11 +1571,7 @@ export default function QRPayPage() { {(limitsValidation.isBlocking || limitsValidation.isWarning) && ( diff --git a/src/app/(mobile-ui)/withdraw/manteca/page.tsx b/src/app/(mobile-ui)/withdraw/manteca/page.tsx index bd3b861bf..7759faddf 100644 --- a/src/app/(mobile-ui)/withdraw/manteca/page.tsx +++ b/src/app/(mobile-ui)/withdraw/manteca/page.tsx @@ -45,10 +45,11 @@ import { } from '@/constants/manteca.consts' import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { TRANSACTIONS } from '@/constants/query.consts' -import { useLimitsValidation, type LimitCurrency } from '@/features/limits/hooks/useLimitsValidation' +import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation' import { MIN_MANTECA_WITHDRAW_AMOUNT } from '@/constants/payment.consts' import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' import { formatExtendedNumber } from '@/utils/general.utils' +import { mapToLimitCurrency, LIMITS_COPY } from '@/features/limits/utils/limits.utils' type MantecaWithdrawStep = 'amountInput' | 'bankDetails' | 'review' | 'success' | 'failure' @@ -104,10 +105,8 @@ export default function MantecaWithdrawFlow() { const { isMantecaKycRequired } = useMantecaKycFlow({ country: selectedCountry }) // determine currency for limits validation - const limitsCurrency = useMemo(() => { - const currency = selectedCountry?.currency?.toUpperCase() - if (currency === 'ARS' || currency === 'BRL') return currency as LimitCurrency - return 'USD' + const limitsCurrency = useMemo(() => { + return mapToLimitCurrency(selectedCountry?.currency) }, [selectedCountry?.currency]) // validate against user's limits @@ -449,11 +448,7 @@ export default function MantecaWithdrawFlow() { {(limitsValidation.isBlocking || limitsValidation.isWarning) && ( @@ -485,8 +485,8 @@ export default function MantecaWithdrawFlow() { > Continue - {/* only show balance error if limits card is not displayed */} - {balanceErrorMessage && !limitsValidation.isBlocking && !limitsValidation.isWarning && ( + {/* only show balance error if limits blocking card is not displayed (warnings can coexist) */} + {balanceErrorMessage && !limitsValidation.isBlocking && ( )} diff --git a/src/app/(mobile-ui)/withdraw/page.tsx b/src/app/(mobile-ui)/withdraw/page.tsx index 01cde8224..bb410b3a2 100644 --- a/src/app/(mobile-ui)/withdraw/page.tsx +++ b/src/app/(mobile-ui)/withdraw/page.tsx @@ -16,6 +16,7 @@ import { useCallback, useEffect, useMemo, useState, useRef, useContext } from 'r import { formatUnits } from 'viem' import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation' import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' +import { LIMITS_COPY } from '@/features/limits/utils/limits.utils' type WithdrawStep = 'inputAmount' | 'selectMethod' @@ -300,11 +301,7 @@ export default function WithdrawPage() { {showLimitsCard && ( @@ -329,8 +331,8 @@ export default function WithdrawPage() { > Continue - {/* only show error if limits card is not displayed */} - {error.showError && !!error.errorMessage && !showLimitsCard && ( + {/* only show error if limits blocking card is not displayed (warnings can coexist) */} + {error.showError && !!error.errorMessage && !limitsValidation.isBlocking && ( )} diff --git a/src/components/AddMoney/components/InputAmountStep.tsx b/src/components/AddMoney/components/InputAmountStep.tsx index 4a387ec7a..517546498 100644 --- a/src/components/AddMoney/components/InputAmountStep.tsx +++ b/src/components/AddMoney/components/InputAmountStep.tsx @@ -10,6 +10,7 @@ import { useCurrency } from '@/hooks/useCurrency' import PeanutLoading from '@/components/Global/PeanutLoading' import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' import { formatExtendedNumber } from '@/utils/general.utils' +import { LIMITS_COPY } from '@/features/limits/utils/limits.utils' type ICurrency = ReturnType @@ -87,11 +88,7 @@ const InputAmountStep = ({ {showLimitsCard && ( )} @@ -118,8 +115,8 @@ const InputAmountStep = ({ > Continue - {/* only show error if limits card is not displayed */} - {error && !showLimitsCard && } + {/* only show error if limits blocking card is not displayed (warnings can coexist) */} + {error && !limitsValidation?.isBlocking && } ) diff --git a/src/components/AddMoney/components/MantecaAddMoney.tsx b/src/components/AddMoney/components/MantecaAddMoney.tsx index 7c03eb8ec..32f66d169 100644 --- a/src/components/AddMoney/components/MantecaAddMoney.tsx +++ b/src/components/AddMoney/components/MantecaAddMoney.tsx @@ -18,7 +18,8 @@ import { MIN_MANTECA_DEPOSIT_AMOUNT } from '@/constants/payment.consts' import { PEANUT_WALLET_TOKEN_DECIMALS } from '@/constants/zerodev.consts' import { TRANSACTIONS } from '@/constants/query.consts' import { useQueryStates, parseAsString, parseAsStringEnum } from 'nuqs' -import { useLimitsValidation, type LimitCurrency } from '@/features/limits/hooks/useLimitsValidation' +import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation' +import { mapToLimitCurrency } from '@/features/limits/utils/limits.utils' // Step type for URL state type MantecaStep = 'inputAmount' | 'depositDetails' @@ -69,10 +70,8 @@ const MantecaAddMoney: FC = () => { const { user, fetchUser } = useAuth() // determine currency for limits validation - const limitsCurrency = useMemo(() => { - const currency = selectedCountry?.currency?.toUpperCase() - if (currency === 'ARS' || currency === 'BRL') return currency as LimitCurrency - return 'USD' + const limitsCurrency = useMemo(() => { + return mapToLimitCurrency(selectedCountry?.currency) }, [selectedCountry?.currency]) // validate against user's limits diff --git a/src/features/limits/components/LimitsWarningCard.tsx b/src/features/limits/components/LimitsWarningCard.tsx index c0004fa31..8f317c52f 100644 --- a/src/features/limits/components/LimitsWarningCard.tsx +++ b/src/features/limits/components/LimitsWarningCard.tsx @@ -5,6 +5,7 @@ import { Icon, type IconProps } from '@/components/Global/Icons/Icon' import Link from 'next/link' import { useModalsContext } from '@/context/ModalsContext' import { twMerge } from 'tailwind-merge' +import { LIMITS_COPY } from '../utils/limits.utils' export type LimitsWarningType = 'warning' | 'error' @@ -23,8 +24,6 @@ interface LimitsWarningCardProps { className?: string } -const SUPPORT_MESSAGE = 'Hi, I would like to increase my payment limits.' - /** * reusable card for displaying limit warnings (yellow) or blocking errors (red) * used across qr payments, add money, and withdraw flows @@ -68,7 +67,7 @@ export default function LimitsWarningCard({ <>