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:
Ahmad Abdolsaheb
2021-11-03 20:32:03 +03:00
committed by GitHub
parent 77ff095d89
commit 9179b2fc55
5 changed files with 120 additions and 30 deletions

View File

@ -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({

View File

@ -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,24 +218,49 @@ 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 {
if (intent_status === 'requires_source_action')
throw {
type: 'subscriptionCreationFailed',
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'
};
}

View File

@ -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
});
}

View File

@ -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}>

View File

@ -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;