fix(client): replace Stripe with PayPal (#41924)

* feat: remove stripe payment option from client

* feat: remove stripe completely

* fix: remove last Stripe remnants

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2021-05-03 11:45:23 +03:00
committed by GitHub
parent 23564eb732
commit 27c8d564e4
32 changed files with 95 additions and 908 deletions

View File

@ -16574,24 +16574,6 @@
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true
},
"stripe": {
"version": "6.36.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-6.36.0.tgz",
"integrity": "sha512-7vjyVO4NCWvX38CH1AuSQH16uRxcQN+UhUTBPs4UHsIl5+SJXLBvCsHrMgd+bY9k1YDliT0fQB1fH9OI3GrEhw==",
"requires": {
"lodash.isplainobject": "^4.0.6",
"qs": "^6.6.0",
"safe-buffer": "^5.1.1",
"uuid": "^3.3.2"
},
"dependencies": {
"qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
}
}
},
"strong-error-handler": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-3.5.0.tgz",

View File

@ -69,7 +69,6 @@
"passport-mock-strategy": "^2.0.0",
"query-string": "^6.14.0",
"rx": "^4.1.0",
"stripe": "^6.36.0",
"uuid": "^3.4.0",
"validator": "^9.4.1"
},

View File

@ -1,231 +1,19 @@
import Stripe from 'stripe';
import debug from 'debug';
import { isEmail, isNumeric } from 'validator';
import {
getAsyncPaypalToken,
verifyWebHook,
updateUser,
verifyWebHookType
} from '../utils/donation';
import {
durationKeysConfig,
donationOneTimeConfig,
donationSubscriptionConfig,
durationsConfig,
onetimeSKUConfig,
donationUrls
} from '../../../../config/donation-settings';
import keys from '../../../../config/secrets';
import { deploymentEnv } from '../../../../config/env';
const log = debug('fcc:boot:donate');
export default function donateBoot(app, done) {
let stripe = false;
const { User } = app.models;
const api = app.loopback.Router();
const hooks = app.loopback.Router();
const donateRouter = app.loopback.Router();
const subscriptionPlans = Object.keys(
donationSubscriptionConfig.plans
).reduce(
(prevDuration, duration) =>
prevDuration.concat(
donationSubscriptionConfig.plans[duration].reduce(
(prevAmount, amount) =>
prevAmount.concat({
amount: amount,
interval: duration,
product: {
name: `${
donationSubscriptionConfig.duration[duration]
} Donation to freeCodeCamp.org - Thank you ($${amount / 100})`,
metadata: {
/* eslint-disable camelcase */
sb_service: `freeCodeCamp.org`,
sb_tier: `${donationSubscriptionConfig.duration[duration]} $${
amount / 100
} Donation`
/* eslint-enable camelcase */
}
},
currency: 'usd',
id: `${donationSubscriptionConfig.duration[
duration
].toLowerCase()}-donation-${amount}`
}),
[]
)
),
[]
);
function validStripeForm(amount, duration, email) {
return isEmail('' + email) &&
isNumeric('' + amount) &&
durationKeysConfig.includes(duration) &&
duration === 'onetime'
? donationOneTimeConfig.includes(amount)
: donationSubscriptionConfig.plans[duration];
}
function connectToStripe() {
return new Promise(function (resolve) {
// connect to stripe API
stripe = Stripe(keys.stripe.secret);
// parse stripe plans
stripe.plans.list({}, function (err, stripePlans) {
if (err) {
throw err;
}
const requiredPlans = subscriptionPlans.map(plan => plan.id);
const availablePlans = stripePlans.data.map(plan => plan.id);
if (process.env.STRIPE_CREATE_PLANS === 'true') {
requiredPlans.forEach(requiredPlan => {
if (!availablePlans.includes(requiredPlan)) {
createStripePlan(
subscriptionPlans.find(plan => plan.id === requiredPlan)
);
}
});
} else {
log(`Skipping plan creation`);
}
});
resolve();
});
}
function createStripePlan(plan) {
log(`Creating subscription plan: ${plan.product.name}`);
stripe.plans.create(plan, function (err) {
if (err) {
log(err);
}
log(`Created plan with plan id: ${plan.id}`);
return;
});
}
function createStripeDonation(req, res) {
const { user, body } = req;
const {
amount,
duration,
token: { email, id }
} = body;
if (!validStripeForm(amount, duration, email)) {
return res.status(500).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, isNew) => {
log('createing a new donating user instance: ', isNew);
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 = user => {
donatingUser = user;
return stripe.customers.create({
email,
card: id
});
};
const createSubscription = customer => {
donation.customerId = customer.id;
return stripe.subscriptions.create({
customer: customer.id,
items: [
{
plan: `${donationSubscriptionConfig.duration[
duration
].toLowerCase()}-donation-${amount}`
}
]
});
};
const createOneTimeCharge = customer => {
donation.customerId = customer.id;
return stripe.charges.create({
amount: amount,
currency: 'usd',
customer: customer.id
});
};
const createAsyncUserDonation = () => {
donatingUser
.createDonation(donation)
.toPromise()
.catch(err => {
throw new Error(err);
});
};
return Promise.resolve(fccUser)
.then(nonDonatingUser => {
const { isDonating } = nonDonatingUser;
if (isDonating && duration !== 'onetime') {
throw {
message: `User already has active recurring donation(s).`,
type: 'AlreadyDonatingError'
};
}
return nonDonatingUser;
})
.then(createCustomer)
.then(customer => {
return duration === 'onetime'
? createOneTimeCharge(customer).then(charge => {
donation.subscriptionId = 'one-time-charge-prefix-' + charge.id;
return res.send(charge);
})
: createSubscription(customer).then(subscription => {
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.' });
});
}
function addDonation(req, res) {
const { user, body } = req;
@ -250,53 +38,6 @@ export default function donateBoot(app, done) {
});
}
async function createStripeSession(req, res) {
const {
body,
body: { donationAmount, donationDuration }
} = req;
if (!body) {
return res
.status(500)
.send({ type: 'danger', message: 'Request has not completed.' });
}
const isSubscription = donationDuration !== 'onetime';
const getSKUId = () => {
const { id } = onetimeSKUConfig[deploymentEnv || 'staging'].find(
skuConfig => skuConfig.amount === `${donationAmount}`
);
return id;
};
const price = isSubscription
? `${durationsConfig[donationDuration]}-donation-${donationAmount}`
: getSKUId();
/* eslint-disable camelcase */
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price,
quantity: 1
}
],
metadata: { ...body },
mode: isSubscription ? 'subscription' : 'payment',
success_url: donationUrls.successUrl,
cancel_url: donationUrls.cancelUrl
});
/* eslint-enable camelcase */
return res.status(200).json({ id: session.id });
} catch (err) {
log(err.message);
return res.status(500).send({
type: 'danger',
message: 'Something went wrong.'
});
}
}
function updatePaypal(req, res) {
const { headers, body } = req;
return Promise.resolve(req)
@ -311,13 +52,8 @@ export default function donateBoot(app, done) {
.finally(() => res.status(200).json({ message: 'received paypal hook' }));
}
const stripeKey = keys.stripe.public;
const secKey = keys.stripe.secret;
const paypalKey = keys.paypal.client;
const paypalSec = keys.paypal.secret;
const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard';
const stripPublicInvalid =
!stripeKey || stripeKey === 'pk_from_stripe_dashboard';
const paypalSecretInvalid =
!paypalKey || paypalKey === 'id_from_paypal_dashboard';
@ -325,22 +61,19 @@ export default function donateBoot(app, done) {
!paypalSec || paypalSec === 'secret_from_paypal_dashboard';
const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid;
const stripeInvalid = stripeSecretInvalid || stripPublicInvalid;
if (stripeInvalid || paypalInvalid) {
if (paypalInvalid) {
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
throw new Error('Donation API keys are required to boot the server!');
}
log('Donation disabled in development unless ALL test keys are provided');
done();
} else {
api.post('/charge-stripe', createStripeDonation);
api.post('/create-stripe-session', createStripeSession);
api.post('/add-donation', addDonation);
hooks.post('/update-paypal', updatePaypal);
donateRouter.use('/donate', api);
donateRouter.use('/hooks', hooks);
app.use(donateRouter);
connectToStripe().then(done);
done();
}
}

View File

@ -12,9 +12,7 @@ export default function getCsurf() {
const { path } = req;
if (
// eslint-disable-next-line max-len
/^\/hooks\/update-paypal$|^\/hooks\/update-stripe$|^\/donate\/charge-stripe$/.test(
path
)
/^\/hooks\/update-paypal$/.test(path)
) {
return next();
}

View File

@ -23,11 +23,7 @@ const signinRE = /^\/signin/;
const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/;
const createStripeSession = /^\/donate\/create-stripe-session/;
// note: this would be replaced by webhooks later
const donateRE = /^\/donate\/charge-stripe$/;
const updateHooksRE = /^\/hooks\/update-paypal$/;
const _pathsAllowedREs = [
authRE,
@ -41,9 +37,7 @@ const _pathsAllowedREs = [
statusRE,
unsubscribedRE,
unsubscribeRE,
updateHooksRE,
donateRE,
createStripeSession
updateHooksRE
];
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {

View File

@ -47,7 +47,7 @@ describe('request-authorization', () => {
const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/;
const updateHooksRE = /^\/hooks\/update-paypal$/;
const allowedPathsList = [
authRE,
@ -79,11 +79,9 @@ describe('request-authorization', () => {
allowedPathsList
);
const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList);
const resultD = isAllowedPath('/hooks/update-stripe', allowedPathsList);
expect(resultA).toBe(true);
expect(resultB).toBe(true);
expect(resultC).toBe(true);
expect(resultD).toBe(true);
});
it('returns false for a non-white-listed path', () => {