From d199d7d085101d75893a310ed3539ccfa064d4ec Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Mon, 9 Feb 2026 14:16:39 +0000 Subject: [PATCH] Validate donation amount to prevent Stripe Elements overflow error Large custom amounts (e.g. 10 quintillion) caused currency.js intValue to produce 1e+21 in scientific notation, which Stripe rejects. Add max validation in CustomAmountInput (client-side) and PaymentIntentsController (server-side) capped at Stripe's $999,999.99 limit. Closes #8484 Co-Authored-By: Claude Opus 4.6 --- .../payments/payment_intents_controller.rb | 7 +++++ .../donation-form/CustomAmountInput.tsx | 12 +++++++- .../payment_intents_controller_test.rb | 30 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/payments/payment_intents_controller.rb b/app/controllers/api/payments/payment_intents_controller.rb index fd06c47c4b..dd3055436e 100644 --- a/app/controllers/api/payments/payment_intents_controller.rb +++ b/app/controllers/api/payments/payment_intents_controller.rb @@ -1,7 +1,14 @@ class API::Payments::PaymentIntentsController < API::BaseController before_action :authenticate_user! + MAX_AMOUNT_IN_CENTS = 99_999_999 + def create + amount_in_cents = params[:amount_in_cents].to_i + unless amount_in_cents.between?(1, MAX_AMOUNT_IN_CENTS) + return render json: { error: "Amount must be between 1 and #{MAX_AMOUNT_IN_CENTS} cents" }, status: :ok + end + payment_intent = ::Payments::Stripe::PaymentIntent::Create.( current_user || params[:email], params[:type], diff --git a/app/javascript/components/donations/donation-form/CustomAmountInput.tsx b/app/javascript/components/donations/donation-form/CustomAmountInput.tsx index 65c4cccb98..0ac0cd45ce 100644 --- a/app/javascript/components/donations/donation-form/CustomAmountInput.tsx +++ b/app/javascript/components/donations/donation-form/CustomAmountInput.tsx @@ -1,6 +1,8 @@ import React, { useCallback } from 'react' import currency from 'currency.js' +const MAX_AMOUNT = '999999.99' + export const CustomAmountInput = ({ onChange, selected, @@ -8,6 +10,7 @@ export const CustomAmountInput = ({ defaultValue, value, min = '0', + max = MAX_AMOUNT, className = '', onBlur, }: { @@ -18,6 +21,7 @@ export const CustomAmountInput = ({ defaultValue?: currency value?: currency | string min?: string + max?: string className?: string }): JSX.Element => { const handleCustomAmountChange = useCallback( @@ -34,9 +38,14 @@ export const CustomAmountInput = ({ return } + if (parseFloat(e.target.value) > parseFloat(max)) { + onChange(currency(NaN)) + return + } + onChange(currency(e.target.value)) }, - [onChange] + [onChange, max] ) const classNames = [ @@ -51,6 +60,7 @@ export const CustomAmountInput = ({