feat: add stripe authentication support (#44060)
* feat: add stripe authentication support Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -30,8 +30,13 @@ export default function donateBoot(app, done) {
|
||||
|
||||
async function handleStripeCardDonation(req, res) {
|
||||
return createStripeCardDonation(req, res, stripe, app).catch(err => {
|
||||
if (err.type === 'AlreadyDonatingError')
|
||||
if (
|
||||
err.type === 'AlreadyDonatingError' ||
|
||||
err.type === 'UserActionRequired' ||
|
||||
err.type === 'PaymentMethodRequired'
|
||||
) {
|
||||
return res.status(402).send({ error: err });
|
||||
}
|
||||
if (err.type === 'InvalidRequest')
|
||||
return res.status(400).send({ error: err });
|
||||
return res.status(500).send({
|
||||
|
@ -179,16 +179,12 @@ export async function updateUser(body, app) {
|
||||
|
||||
export async function createStripeCardDonation(req, res, stripe) {
|
||||
const {
|
||||
body: {
|
||||
token: { id: tokenId },
|
||||
amount,
|
||||
duration
|
||||
},
|
||||
body: { paymentMethodId, amount, duration },
|
||||
user: { name, id: userId, email },
|
||||
user
|
||||
} = req;
|
||||
|
||||
if (!tokenId || !amount || !duration || !userId || !email) {
|
||||
if (!paymentMethodId || !amount || !duration || !userId || !email) {
|
||||
throw {
|
||||
message: 'Request is not valid',
|
||||
type: 'InvalidRequest'
|
||||
@ -210,7 +206,8 @@ export async function createStripeCardDonation(req, res, stripe) {
|
||||
try {
|
||||
const customer = await stripe.customers.create({
|
||||
email,
|
||||
card: tokenId,
|
||||
payment_method: paymentMethodId,
|
||||
invoice_settings: { default_payment_method: paymentMethodId },
|
||||
...(name && { name })
|
||||
});
|
||||
customerId = customer?.id;
|
||||
@ -221,26 +218,51 @@ export async function createStripeCardDonation(req, res, stripe) {
|
||||
};
|
||||
}
|
||||
log(`Stripe customer with id ${customerId} created`);
|
||||
// log creation of Stripe customer event
|
||||
|
||||
let subscriptionId;
|
||||
try {
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
const {
|
||||
id: subscription_id,
|
||||
latest_invoice: {
|
||||
payment_intent: { client_secret, status: intent_status }
|
||||
}
|
||||
} = await stripe.subscriptions.create({
|
||||
// create Stripe subscription
|
||||
customer: customerId,
|
||||
payment_behavior: 'allow_incomplete',
|
||||
items: [
|
||||
{
|
||||
plan: `${donationSubscriptionConfig.duration[
|
||||
duration
|
||||
].toLowerCase()}-donation-${amount}`
|
||||
}
|
||||
]
|
||||
],
|
||||
expand: ['latest_invoice.payment_intent']
|
||||
});
|
||||
subscriptionId = subscription?.id;
|
||||
} catch {
|
||||
throw {
|
||||
type: 'subscriptionCreationFailed',
|
||||
message: 'Failed to create stripe subscription'
|
||||
};
|
||||
|
||||
if (intent_status === 'requires_source_action')
|
||||
throw {
|
||||
type: 'UserActionRequired',
|
||||
message: 'Payment requires user action',
|
||||
client_secret
|
||||
};
|
||||
else if (intent_status === 'requires_source')
|
||||
throw {
|
||||
type: 'PaymentMethodRequired',
|
||||
message: 'Card has been declined'
|
||||
};
|
||||
subscriptionId = subscription_id;
|
||||
} catch (err) {
|
||||
if (
|
||||
err.type === 'UserActionRequired' ||
|
||||
err.type === 'PaymentMethodRequired'
|
||||
)
|
||||
throw err;
|
||||
else
|
||||
throw {
|
||||
type: 'SubscriptionCreationFailed',
|
||||
message: 'Failed to create stripe subscription'
|
||||
};
|
||||
}
|
||||
log(`Stripe subscription with id ${subscriptionId} created`);
|
||||
|
||||
|
@ -29,7 +29,7 @@ import Spacer from '../helpers/spacer';
|
||||
import DonateCompletion from './DonateCompletion';
|
||||
import type { AddDonationData } from './PaypalButton';
|
||||
import PaypalButton from './PaypalButton';
|
||||
import StripeCardForm from './stripe-card-form';
|
||||
import StripeCardForm, { HandleAuthentication } from './stripe-card-form';
|
||||
import WalletsWrapper from './walletsButton';
|
||||
|
||||
import './Donation.css';
|
||||
@ -57,9 +57,10 @@ type DonateFormProps = {
|
||||
addDonation: (data: unknown) => unknown;
|
||||
postChargeStripe: (data: unknown) => unknown;
|
||||
postChargeStripeCard: (data: {
|
||||
token: Token;
|
||||
paymentMethodId: string;
|
||||
amount: number;
|
||||
duration: string;
|
||||
handleAuthentication: HandleAuthentication;
|
||||
}) => void;
|
||||
defaultTheme?: string;
|
||||
email: string;
|
||||
@ -221,7 +222,10 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
||||
});
|
||||
}
|
||||
|
||||
postStripeCardDonation(token: Token) {
|
||||
postStripeCardDonation(
|
||||
paymentMethodId: string,
|
||||
handleAuthentication: HandleAuthentication
|
||||
) {
|
||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
||||
this.props.handleProcessing(
|
||||
duration,
|
||||
@ -229,9 +233,10 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
||||
'Stripe card payment submission'
|
||||
);
|
||||
this.props.postChargeStripeCard({
|
||||
token,
|
||||
paymentMethodId,
|
||||
amount,
|
||||
duration
|
||||
duration,
|
||||
handleAuthentication
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,9 @@ import {
|
||||
} from '@stripe/react-stripe-js';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import type {
|
||||
Token,
|
||||
StripeCardNumberElementChangeEvent,
|
||||
StripeCardExpiryElementChangeEvent
|
||||
StripeCardExpiryElementChangeEvent,
|
||||
PaymentIntentResult
|
||||
} from '@stripe/stripe-js';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
@ -19,9 +19,18 @@ import envData from '../../../../config/env.json';
|
||||
import { AddDonationData } from './PaypalButton';
|
||||
|
||||
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
|
||||
|
||||
export type HandleAuthentication = (
|
||||
clientSecret: string,
|
||||
paymentMethod: string
|
||||
) => Promise<PaymentIntentResult | { error: { type: string } }>;
|
||||
|
||||
interface FormPropTypes {
|
||||
onDonationStateChange: (donationState: AddDonationData) => void;
|
||||
postStripeCardDonation: (token: Token) => void;
|
||||
postStripeCardDonation: (
|
||||
paymentMethodId: string,
|
||||
handleAuthentication: HandleAuthentication
|
||||
) => void;
|
||||
t: (label: string) => string;
|
||||
theme: string;
|
||||
processing: boolean;
|
||||
@ -100,7 +109,10 @@ const StripeCardForm = ({
|
||||
const cardElement = elements.getElement(CardNumberElement);
|
||||
if (cardElement) {
|
||||
setTokenizing(true);
|
||||
const { error, token } = await stripe.createToken(cardElement);
|
||||
const { paymentMethod, error } = await stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card: cardElement
|
||||
});
|
||||
if (error) {
|
||||
onDonationStateChange({
|
||||
redirecting: false,
|
||||
@ -108,11 +120,24 @@ const StripeCardForm = ({
|
||||
success: false,
|
||||
error: t('donate.went-wrong')
|
||||
});
|
||||
} else if (token) postStripeCardDonation(token);
|
||||
} else if (paymentMethod)
|
||||
postStripeCardDonation(paymentMethod.id, handleAuthentication);
|
||||
}
|
||||
}
|
||||
return setTokenizing(false);
|
||||
};
|
||||
const handleAuthentication = async (
|
||||
clientSecret: string,
|
||||
paymentMethod: string
|
||||
) => {
|
||||
if (stripe) {
|
||||
return stripe.confirmCardPayment(clientSecret, {
|
||||
// eslint-disable-next-line camelcase
|
||||
payment_method: paymentMethod
|
||||
});
|
||||
}
|
||||
return { error: { type: 'StripeNotLoaded' } };
|
||||
};
|
||||
|
||||
return (
|
||||
<Form className='donation-form' onSubmit={handleSubmit}>
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
call,
|
||||
take
|
||||
} from 'redux-saga/effects';
|
||||
|
||||
import {
|
||||
addDonation,
|
||||
postChargeStripe,
|
||||
@ -73,10 +74,42 @@ function* postChargeStripeSaga({ payload }) {
|
||||
}
|
||||
}
|
||||
|
||||
function* postChargeStripeCardSaga({ payload }) {
|
||||
function* stripeCardErrorHandler(
|
||||
error,
|
||||
handleAuthentication,
|
||||
clientSecret,
|
||||
paymentMethodId
|
||||
) {
|
||||
if (error.type === 'UserActionRequired' && clientSecret) {
|
||||
yield handleAuthentication(clientSecret, paymentMethodId)
|
||||
.then(result => {
|
||||
if (result?.paymentIntent?.status !== 'succeeded')
|
||||
throw result.error || { type: 'StripeAuthorizationFailed' };
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function* postChargeStripeCardSaga({
|
||||
payload: { paymentMethodId, amount, duration, handleAuthentication }
|
||||
}) {
|
||||
try {
|
||||
const { error } = yield call(postChargeStripeCard, payload);
|
||||
if (error) throw error;
|
||||
const optimizedPayload = { paymentMethodId, amount, duration };
|
||||
const { error } = yield call(postChargeStripeCard, optimizedPayload);
|
||||
if (error) {
|
||||
yield stripeCardErrorHandler(
|
||||
error,
|
||||
handleAuthentication,
|
||||
error.client_secret,
|
||||
paymentMethodId,
|
||||
optimizedPayload
|
||||
);
|
||||
}
|
||||
yield call(addDonation, optimizedPayload);
|
||||
yield put(postChargeStripeCardComplete());
|
||||
} catch (error) {
|
||||
const errorMessage = error.message || defaultDonationErrorMessage;
|
||||
|
Reference in New Issue
Block a user