diff --git a/app/commands/donations/payment_intent/create.rb b/app/commands/donations/payment_intent/create.rb index 67f6876cbe..d0e569763c 100644 --- a/app/commands/donations/payment_intent/create.rb +++ b/app/commands/donations/payment_intent/create.rb @@ -1,4 +1,7 @@ module Donations + class PaymentIntentError < RuntimeError + end + module PaymentIntent class Create include Mandate diff --git a/app/controllers/api/donations/payment_intents_controller.rb b/app/controllers/api/donations/payment_intents_controller.rb index 4b25b9ffab..b1118a789e 100644 --- a/app/controllers/api/donations/payment_intents_controller.rb +++ b/app/controllers/api/donations/payment_intents_controller.rb @@ -14,6 +14,12 @@ def create client_secret: payment_intent.client_secret } } + rescue Stripe::InvalidRequestError => e + # React currently can't handle this being + # anything other than a 200 + render json: { + error: e.message + }, status: :ok end def succeeded diff --git a/app/javascript/components/donations/Form.tsx b/app/javascript/components/donations/Form.tsx index d3dbbc8528..6445aec119 100644 --- a/app/javascript/components/donations/Form.tsx +++ b/app/javascript/components/donations/Form.tsx @@ -1,8 +1,10 @@ -import React, { useState, createContext } from 'react' +import React, { useState, createContext, useCallback, useMemo } from 'react' import { PaymentIntentType } from './StripeForm' import { Tab, TabContext } from '../common/Tab' import { TransactionForm } from './TransactionForm' import { ExistingSubscriptionNotice } from './ExistingSubscriptionNotice' +import { ExercismStripeElements } from './donation-form/ExercismStripeElements' +import { StripeForm } from './StripeForm' const TabsContext = createContext({ current: 'subscription', @@ -13,6 +15,9 @@ type Links = { settings: string } +const PAYMENT_DEFAULT_AMOUNT_IN_DOLLARS = 32 +const SUBSCRIPTION_DEFAULT_AMOUNT_IN_DOLLARS = 32 + export const Form = ({ existingSubscriptionAmountinDollars, onSuccess, @@ -22,10 +27,49 @@ export const Form = ({ onSuccess: (type: PaymentIntentType, amountInDollars: number) => void links: Links }): JSX.Element => { + const [amountInDollars, setAmountInDollars] = useState({ + subscription: SUBSCRIPTION_DEFAULT_AMOUNT_IN_DOLLARS, + payment: PAYMENT_DEFAULT_AMOUNT_IN_DOLLARS, + }) const [transactionType, setTransactionType] = useState( existingSubscriptionAmountinDollars ? 'payment' : 'subscription' ) + const handleAmountChange = useCallback( + (transactionType: PaymentIntentType) => { + return (value: number) => { + switch (transactionType) { + case 'subscription': + setAmountInDollars({ + ...amountInDollars, + subscription: isNaN(value) + ? SUBSCRIPTION_DEFAULT_AMOUNT_IN_DOLLARS + : value, + }) + + break + case 'payment': + setAmountInDollars({ + ...amountInDollars, + payment: isNaN(value) ? PAYMENT_DEFAULT_AMOUNT_IN_DOLLARS : value, + }) + + break + } + } + }, + [amountInDollars] + ) + + const currentAmountInDollars = useMemo(() => { + switch (transactionType) { + case 'payment': + return amountInDollars.payment + case 'subscription': + return amountInDollars.subscription + } + }, [amountInDollars, transactionType]) + return ( {existingSubscriptionAmountinDollars != null ? ( + + + diff --git a/app/javascript/components/donations/StripeForm.tsx b/app/javascript/components/donations/StripeForm.tsx index 972730a834..e8952a21d4 100644 --- a/app/javascript/components/donations/StripeForm.tsx +++ b/app/javascript/components/donations/StripeForm.tsx @@ -102,7 +102,13 @@ export function StripeForm({ type: paymentIntentType, amount_in_dollars: amountInDollars, }), - }).then((data: any) => data.paymentIntent) + }).then((data: any) => { + if (data.error) { + setError(`Payment failed with error: ${data.error}`) + return null + } + return data.paymentIntent + }) }, [paymentIntentType, amountInDollars]) const handleSubmit = async (event: React.ChangeEvent) => { @@ -118,6 +124,12 @@ export function StripeForm({ setProcessing(true) getPaymentRequest().then(async (paymentIntent: PaymentIntent) => { + // If we've failed to get a payment intent get out of here + if (paymentIntent === undefined || paymentIntent === null) { + setProcessing(false) + return + } + // Get a reference to a mounted CardElement. Elements knows how // to find the CardElement because there can only ever be one of // each type of element. We could maybe use a ref here instead? @@ -156,7 +168,7 @@ export function StripeForm({