-
Notifications
You must be signed in to change notification settings - Fork 13
feat: limits cards in payment flows #1621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9bfaeab
f35d994
3bf6517
2170000
2ec238d
f9c8ed2
269626b
252c8e6
8107d89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,10 @@ 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 { getLimitsWarningCardProps } from '@/features/limits/utils' | ||
| import { useExchangeRate } from '@/hooks/useExchangeRate' | ||
|
|
||
| // Step type for URL state | ||
| type BridgeBankStep = 'inputAmount' | 'kyc' | 'collectUserDetails' | 'showDetails' | ||
|
|
@@ -97,6 +101,42 @@ export default function OnrampBankPage() { | |
| return getMinimumAmount(selectedCountry.id) | ||
| }, [selectedCountry?.id]) | ||
|
|
||
| // get local currency for the selected country (EUR, MXN, USD) | ||
| const localCurrency = useMemo(() => { | ||
| if (!selectedCountry?.id) return 'USD' | ||
| return getCurrencyConfig(selectedCountry.id, 'onramp').currency.toUpperCase() | ||
| }, [selectedCountry?.id]) | ||
|
|
||
| // get exchange rate: local currency → USD (for limits validation) | ||
| // skip for USD since it's 1:1 | ||
| const { exchangeRate, isLoading: isRateLoading } = useExchangeRate({ | ||
| sourceCurrency: localCurrency, | ||
| destinationCurrency: 'USD', | ||
| enabled: localCurrency !== 'USD', | ||
| }) | ||
|
|
||
| // convert input amount to USD for limits validation | ||
| // bridge limits are always in USD, but user inputs in local currency | ||
| const usdEquivalent = useMemo(() => { | ||
| if (!rawTokenAmount) return 0 | ||
| const numericAmount = parseFloat(rawTokenAmount.replace(/,/g, '')) | ||
| if (isNaN(numericAmount)) return 0 | ||
|
|
||
| // for USD, no conversion needed | ||
| if (localCurrency === 'USD') return numericAmount | ||
|
|
||
| // convert local currency to USD | ||
| return exchangeRate > 0 ? numericAmount * exchangeRate : 0 | ||
| }, [rawTokenAmount, localCurrency, exchangeRate]) | ||
|
|
||
| // validate against user's bridge limits | ||
| // uses USD equivalent to correctly compare against USD-denominated limits | ||
| const limitsValidation = useLimitsValidation({ | ||
| flowType: 'onramp', | ||
| amount: usdEquivalent, | ||
| currency: 'USD', | ||
| }) | ||
|
Comment on lines
+132
to
+138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against limits loading state. The Per the PR review comments recommendation: "Disable actions while limitsValidation.isLoading is true." Suggested fix in button disabled state (around line 431) disabled={
!parseFloat(rawTokenAmount) ||
parseFloat(rawTokenAmount) < minimumAmount ||
error.showError ||
isCreatingOnramp ||
limitsValidation.isBlocking ||
- (localCurrency !== 'USD' && isRateLoading)
+ (localCurrency !== 'USD' && isRateLoading) ||
+ limitsValidation.isLoading
}🤖 Prompt for AI Agents |
||
|
|
||
| // 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 +381,8 @@ export default function OnrampBankPage() { | |
| } | ||
|
|
||
| if (urlState.step === 'inputAmount') { | ||
| const showLimitsCard = limitsValidation.isBlocking || limitsValidation.isWarning | ||
|
|
||
| return ( | ||
| <div className="flex flex-col justify-start space-y-8"> | ||
| <NavHeader title="Add Money" onPrev={handleBack} /> | ||
|
|
@@ -364,11 +406,24 @@ export default function OnrampBankPage() { | |
| hideBalance | ||
| /> | ||
|
|
||
| <InfoCard | ||
| variant="warning" | ||
| icon="alert" | ||
| description="This must match what you send from your bank!" | ||
| /> | ||
| {/* limits warning/error card */} | ||
| {showLimitsCard && | ||
| (() => { | ||
| const limitsCardProps = getLimitsWarningCardProps({ | ||
| validation: limitsValidation, | ||
| flowType: 'onramp', | ||
| currency: 'USD', | ||
| }) | ||
| return limitsCardProps ? <LimitsWarningCard {...limitsCardProps} /> : null | ||
| })()} | ||
|
|
||
| {!limitsValidation.isBlocking && ( | ||
| <InfoCard | ||
| variant="warning" | ||
| icon="alert" | ||
| description="This must match what you send from your bank!" | ||
| /> | ||
| )} | ||
| <Button | ||
| variant="purple" | ||
| shadowSize="4" | ||
|
|
@@ -377,14 +432,19 @@ export default function OnrampBankPage() { | |
| !parseFloat(rawTokenAmount) || | ||
| parseFloat(rawTokenAmount) < minimumAmount || | ||
| error.showError || | ||
| isCreatingOnramp | ||
| isCreatingOnramp || | ||
| limitsValidation.isBlocking || | ||
| (localCurrency !== 'USD' && isRateLoading) | ||
| } | ||
| className="w-full" | ||
| loading={isCreatingOnramp} | ||
| > | ||
| Continue | ||
| </Button> | ||
| {error.showError && !!error.errorMessage && <ErrorAlert description={error.errorMessage} />} | ||
| {/* only show error if limits blocking card is not displayed (warnings can coexist) */} | ||
| {error.showError && !!error.errorMessage && !limitsValidation.isBlocking && ( | ||
| <ErrorAlert description={error.errorMessage} /> | ||
| )} | ||
| </div> | ||
|
|
||
| <OnrampConfirmationModal | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,10 @@ import { useModalsContext } from '@/context/ModalsContext' | |
| import maintenanceConfig from '@/config/underMaintenance.config' | ||
| import PointsCard from '@/components/Common/PointsCard' | ||
| import { TRANSACTIONS } from '@/constants/query.consts' | ||
| import { useLimitsValidation } from '@/features/limits/hooks/useLimitsValidation' | ||
| import LimitsWarningCard from '@/features/limits/components/LimitsWarningCard' | ||
| import { getLimitsWarningCardProps } from '@/features/limits/utils' | ||
| import useKycStatus from '@/hooks/useKycStatus' | ||
|
|
||
| const MAX_QR_PAYMENT_AMOUNT = '2000' | ||
| const MIN_QR_PAYMENT_AMOUNT = '0.1' | ||
|
|
@@ -118,6 +122,7 @@ export default function QRPayPage() { | |
| }, [paymentProcessor]) | ||
|
|
||
| const { shouldBlockPay, kycGateState } = useQrKycGate(paymentProcessor) | ||
| const { isUserMantecaKycApproved } = useKycStatus() | ||
| const queryClient = useQueryClient() | ||
| const [isShaking, setIsShaking] = useState(false) | ||
| const [shakeIntensity, setShakeIntensity] = useState<ShakeIntensity>('none') | ||
|
|
@@ -383,6 +388,14 @@ export default function QRPayPage() { | |
| } | ||
| }, [paymentProcessor, simpleFiPayment, paymentLock?.code, paymentLock?.paymentAgainstAmount, amount]) | ||
|
|
||
| // validate payment against user's limits | ||
| // currency comes from payment lock/simplefi - hook normalizes it internally | ||
| const limitsValidation = useLimitsValidation({ | ||
| flowType: 'qr-payment', | ||
| amount: usdAmount, | ||
| currency: currency?.code, | ||
| }) | ||
|
|
||
| // 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 +1548,20 @@ export default function QRPayPage() { | |
| hideBalance | ||
| /> | ||
| )} | ||
| {balanceErrorMessage && <ErrorAlert description={balanceErrorMessage} />} | ||
| {/* only show balance error if limits blocking card is not displayed (warnings can coexist) */} | ||
| {balanceErrorMessage && !limitsValidation.isBlocking && ( | ||
| <ErrorAlert description={balanceErrorMessage} /> | ||
| )} | ||
|
|
||
| {/* Limits Warning/Error Card */} | ||
| {(() => { | ||
| const limitsCardProps = getLimitsWarningCardProps({ | ||
| validation: limitsValidation, | ||
| flowType: 'qr-payment', | ||
| currency: limitsValidation.currency, | ||
| }) | ||
| return limitsCardProps ? <LimitsWarningCard {...limitsCardProps} /> : null | ||
| })()} | ||
|
|
||
| {/* Information Card */} | ||
| <Card className="space-y-0 px-4"> | ||
|
|
@@ -1560,7 +1586,8 @@ export default function QRPayPage() { | |
| shouldBlockPay || | ||
| !usdAmount || | ||
| usdAmount === '0.00' || | ||
| isWaitingForWebSocket | ||
| isWaitingForWebSocket || | ||
| limitsValidation.isBlocking | ||
|
Comment on lines
+1589
to
+1590
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider disabling button while limits are loading. Similar to the concern raised in the withdraw flow, the disabled condition doesn't account for 🔧 Suggested fix isWaitingForWebSocket ||
- limitsValidation.isBlocking
+ limitsValidation.isBlocking ||
+ limitsValidation.isLoading🤖 Prompt for AI Agents |
||
| } | ||
| > | ||
| {isLoading || isWaitingForWebSocket | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle exchange rate fetch errors.
When
exchangeRateis0due to a fetch error (not just loading),usdEquivalentwill be0, potentially bypassing limits validation for non-USD currencies. The button's disabled state only guards againstisRateLoading, notisError.Consider also destructuring
isErrorfromuseExchangeRateand either disabling the button or showing an error when the rate fetch fails.Suggested approach
// get exchange rate: local currency → USD (for limits validation) // skip for USD since it's 1:1 - const { exchangeRate, isLoading: isRateLoading } = useExchangeRate({ + const { exchangeRate, isLoading: isRateLoading, isError: isRateError } = useExchangeRate({ sourceCurrency: localCurrency, destinationCurrency: 'USD', enabled: localCurrency !== 'USD', })Then in the button disabled state (line 437):
🤖 Prompt for AI Agents