mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-04-12 10:00:39 -04:00
fix: update stripe wallets to use payment intent (#54668)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import debug from 'debug';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings';
|
||||
import {
|
||||
donationSubscriptionConfig,
|
||||
allStripeProductIdsArray
|
||||
} from '../../../../shared/config/donation-settings';
|
||||
import keys from '../../../config/secrets';
|
||||
import {
|
||||
createStripeCardDonation,
|
||||
handleStripeCardUpdateSession
|
||||
handleStripeCardUpdateSession,
|
||||
inLastFiveMinutes
|
||||
} from '../utils/donation';
|
||||
import { validStripeForm } from '../utils/stripeHelpers';
|
||||
|
||||
@@ -42,117 +46,82 @@ export default function donateBoot(app, done) {
|
||||
});
|
||||
}
|
||||
|
||||
function createStripeDonation(req, res) {
|
||||
const { user, body } = req;
|
||||
async function createStripeDonation(req, res) {
|
||||
const { body } = req;
|
||||
const { amount, duration, email, subscriptionId } = body;
|
||||
try {
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
const isSubscriptionActive = subscription.status === 'active';
|
||||
const productId = subscription.items.data[0].plan.product;
|
||||
const isStartedRecently = inLastFiveMinutes(
|
||||
subscription.current_period_start
|
||||
);
|
||||
const isProductIdValid = allStripeProductIdsArray.includes(productId);
|
||||
|
||||
const {
|
||||
amount,
|
||||
duration,
|
||||
token: { id },
|
||||
email,
|
||||
name
|
||||
} = body;
|
||||
if (isSubscriptionActive && isProductIdValid && isStartedRecently) {
|
||||
const [donatingUser] = await User.findOrCreate(
|
||||
{ where: { email } },
|
||||
{ email }
|
||||
);
|
||||
const donation = {
|
||||
email,
|
||||
amount,
|
||||
duration,
|
||||
provider: 'stripe',
|
||||
subscriptionId,
|
||||
customerId: subscription.customer,
|
||||
startDate: new Date().toISOString()
|
||||
};
|
||||
await donatingUser.createDonation(donation);
|
||||
return res.status(200).send({ isDonating: true });
|
||||
} else {
|
||||
throw new Error('Donation failed due to a server error.');
|
||||
}
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.send({ error: 'Donation failed due to a server error.' });
|
||||
}
|
||||
}
|
||||
|
||||
async function createStripePaymentIntent(req, res) {
|
||||
const { body } = req;
|
||||
const { amount, duration, email, name } = body;
|
||||
|
||||
if (!validStripeForm(amount, duration, email)) {
|
||||
return res.status(500).send({
|
||||
return res.status(400).send({
|
||||
error: 'The donation form had invalid values for this submission.'
|
||||
});
|
||||
}
|
||||
|
||||
const fccUser = user
|
||||
? Promise.resolve(user)
|
||||
: new Promise((resolve, reject) =>
|
||||
User.findOrCreate(
|
||||
{ where: { email } },
|
||||
{ email },
|
||||
(err, instance) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(instance);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
let donatingUser = {};
|
||||
let donation = {
|
||||
email,
|
||||
amount,
|
||||
duration,
|
||||
provider: 'stripe',
|
||||
startDate: new Date(Date.now()).toISOString()
|
||||
};
|
||||
|
||||
const createCustomer = async user => {
|
||||
let customer;
|
||||
donatingUser = user;
|
||||
try {
|
||||
customer = await stripe.customers.create({
|
||||
email,
|
||||
card: id,
|
||||
name
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error('Error creating stripe customer');
|
||||
}
|
||||
log(`Stripe customer with id ${customer.id} created`);
|
||||
return customer;
|
||||
};
|
||||
|
||||
const createSubscription = async customer => {
|
||||
donation.customerId = customer.id;
|
||||
let sub;
|
||||
try {
|
||||
sub = await stripe.subscriptions.create({
|
||||
customer: customer.id,
|
||||
items: [
|
||||
{
|
||||
plan: `${donationSubscriptionConfig.duration[
|
||||
duration
|
||||
].toLowerCase()}-donation-${amount}`
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error('Error creating stripe subscription');
|
||||
}
|
||||
return sub;
|
||||
};
|
||||
|
||||
const createAsyncUserDonation = () => {
|
||||
donatingUser
|
||||
.createDonation(donation)
|
||||
.toPromise()
|
||||
.catch(err => {
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return Promise.resolve(fccUser)
|
||||
.then(nonDonatingUser => {
|
||||
// the logic is removed since users can donate without an account
|
||||
return nonDonatingUser;
|
||||
})
|
||||
.then(createCustomer)
|
||||
.then(customer => {
|
||||
return createSubscription(customer).then(subscription => {
|
||||
log(`Stripe subscription with id ${subscription.id} created`);
|
||||
donation.subscriptionId = subscription.id;
|
||||
return res.send(subscription);
|
||||
});
|
||||
})
|
||||
.then(createAsyncUserDonation)
|
||||
.catch(err => {
|
||||
if (
|
||||
err.type === 'StripeCardError' ||
|
||||
err.type === 'AlreadyDonatingError'
|
||||
) {
|
||||
return res.status(402).send({ error: err.message });
|
||||
}
|
||||
return res
|
||||
.status(500)
|
||||
.send({ error: 'Donation failed due to a server error.' });
|
||||
try {
|
||||
const stripeCustomer = await stripe.customers.create({
|
||||
email,
|
||||
name
|
||||
});
|
||||
|
||||
const stripeSubscription = await stripe.subscriptions.create({
|
||||
customer: stripeCustomer.id,
|
||||
items: [
|
||||
{
|
||||
plan: `${donationSubscriptionConfig.duration[duration]}-donation-${amount}`
|
||||
}
|
||||
],
|
||||
payment_behavior: 'default_incomplete',
|
||||
payment_settings: { save_default_payment_method: 'on_subscription' },
|
||||
expand: ['latest_invoice.payment_intent']
|
||||
});
|
||||
|
||||
res.status(200).send({
|
||||
subscriptionId: stripeSubscription.id,
|
||||
clientSecret:
|
||||
stripeSubscription.latest_invoice.payment_intent.client_secret
|
||||
});
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.send({ error: 'Donation failed due to a server error.' });
|
||||
}
|
||||
}
|
||||
|
||||
function addDonation(req, res) {
|
||||
@@ -208,6 +177,7 @@ export default function donateBoot(app, done) {
|
||||
} else {
|
||||
api.post('/charge-stripe', createStripeDonation);
|
||||
api.post('/charge-stripe-card', handleStripeCardDonation);
|
||||
api.post('/create-stripe-payment-intent', createStripePaymentIntent);
|
||||
api.put('/update-stripe-card', handleStripeCardUpdate);
|
||||
api.post('/add-donation', addDonation);
|
||||
donateRouter.use('/donate', api);
|
||||
|
||||
@@ -14,7 +14,9 @@ export default function getCsurf() {
|
||||
const { path } = req;
|
||||
if (
|
||||
// eslint-disable-next-line max-len
|
||||
/^\/donate\/charge-stripe$|^\/coderoad-challenge-completed$/.test(path)
|
||||
/^\/donate\/charge-stripe$|^\/donate\/create-stripe-payment-intent$|^\/coderoad-challenge-completed$/.test(
|
||||
path
|
||||
)
|
||||
) {
|
||||
next();
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,7 @@ const unsubscribedRE = /^\/unsubscribed\//;
|
||||
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
|
||||
// note: this would be replaced by webhooks later
|
||||
const donateRE = /^\/donate\/charge-stripe$/;
|
||||
const paymentIntentRE = /^\/donate\/create-stripe-payment-intent$/;
|
||||
const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/;
|
||||
const mobileLoginRE = /^\/mobile-login\/?$/;
|
||||
|
||||
@@ -40,6 +41,7 @@ const _pathsAllowedREs = [
|
||||
unsubscribedRE,
|
||||
unsubscribeRE,
|
||||
donateRE,
|
||||
paymentIntentRE,
|
||||
submitCoderoadChallengeRE,
|
||||
mobileLoginRE
|
||||
];
|
||||
|
||||
@@ -173,3 +173,9 @@ export async function handleStripeCardUpdateSession(req, app, stripe) {
|
||||
});
|
||||
return { sessionId: session.id };
|
||||
}
|
||||
|
||||
export function inLastFiveMinutes(unixTimestamp) {
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
const timeDifference = currentTimestamp - unixTimestamp;
|
||||
return timeDifference <= 300; // 300 seconds is 5 minutes
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { isEmail, isNumeric } from 'validator';
|
||||
import {
|
||||
durationKeysConfig,
|
||||
donationOneTimeConfig,
|
||||
donationSubscriptionConfig
|
||||
} from '../../../../shared/config/donation-settings';
|
||||
import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings';
|
||||
|
||||
export function validStripeForm(amount, duration, email) {
|
||||
return isEmail('' + email) &&
|
||||
return (
|
||||
isEmail('' + email) &&
|
||||
isNumeric('' + amount) &&
|
||||
durationKeysConfig.includes(duration) &&
|
||||
duration === 'one-time'
|
||||
? donationOneTimeConfig.includes(amount)
|
||||
: donationSubscriptionConfig.plans[duration];
|
||||
donationSubscriptionConfig.plans[duration].includes(amount)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user