feat(client): add google pay (#43117)

* feat: initial button setup client

* feat: rename walletsButton to .tsx

* chore: typescriptize wallet component

* chore: re-add keys to config, env, etc + check in gatsby-node

* feat: refactor donate form and wallet component

* feat(client): set labels correctly

* chore: add stripe package back to server

* chore: add stripe back to allowed paths

* chore: copy donate.js code from PR #41924

* feat: attempt to make back end work

* feat: make redux work

* feat: clean up

* feat: hokify

* feat: add error handling

* fix: back-end should be working

* fix: type errors

* fix: clean up back-end

* feat:addd styles

* feat: connect the client to the api

* feat: display wallets button everywhere

* test: add stripe key for cypress action

* test: fix for cypress tests

* test: cypress tests again

* test: maybe?

* test: more

* test: more

* test: more

* test

* askdfjasklfj

* fix: tests finally?

* revert: remove space from cypress yaml action

* remove logs

Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2021-08-08 23:22:25 +03:00
committed by GitHub
parent ad54684dce
commit b623c340a9
23 changed files with 509 additions and 49 deletions

View File

@ -7277,6 +7277,22 @@
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true
},
"stripe": {
"version": "8.168.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-8.168.0.tgz",
"integrity": "sha512-MQXTarijIOagtLajGe1zBFc9KMbB7jIoFv/kr1WsDPJO/S+/hhZjsXCgBkNvnlwK7Yl0VUn+YrgXl9/9wU6WCw==",
"requires": {
"@types/node": ">=8.1.0",
"qs": "^6.6.0"
},
"dependencies": {
"@types/node": {
"version": "16.4.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.12.tgz",
"integrity": "sha512-zxrTNFl9Z8boMJXs6ieqZP0wAhvkdzmHSxTlJabM16cf5G9xBc1uPRH5Bbv2omEDDiM8MzTfqTJXBf0Ba4xFWA=="
}
}
},
"strong-error-handler": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-3.5.0.tgz",

View File

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

View File

@ -1,4 +1,6 @@
import debug from 'debug';
import Stripe from 'stripe';
import { donationSubscriptionConfig } from '../../../../config/donation-settings';
import keys from '../../../../config/secrets';
import {
getAsyncPaypalToken,
@ -6,17 +8,139 @@ import {
updateUser,
verifyWebHookType
} from '../utils/donation';
import { validStripeForm } from '../utils/stripeHelpers';
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();
function addDonation(req, res) {
function connectToStripe() {
return new Promise(function () {
// connect to stripe API
stripe = Stripe(keys.stripe.secret);
});
}
function createStripeDonation(req, res) {
const { user, body } = req;
const {
amount,
duration,
token: { id },
email,
name
} = 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) => {
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.status(200);
});
})
.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;
if (!user || !body) {
return res
.status(500)
@ -52,28 +176,37 @@ 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';
const paypalPublicInvalid =
!paypalSec || paypalSec === 'secret_from_paypal_dashboard';
const stripeInvalid = stripeSecretInvalid || stripPublicInvalid;
const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid;
if (paypalInvalid) {
if (stripeInvalid || 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('/add-donation', addDonation);
hooks.post('/update-paypal', updatePaypal);
donateRouter.use('/donate', api);
donateRouter.use('/hooks', hooks);
app.use(donateRouter);
connectToStripe(stripe).then(done);
done();
}
}

View File

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

View File

@ -23,7 +23,10 @@ const signinRE = /^\/signin/;
const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$/;
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 _pathsAllowedREs = [
authRE,
@ -37,7 +40,9 @@ const _pathsAllowedREs = [
statusRE,
unsubscribedRE,
unsubscribeRE,
updateHooksRE
updateHooksRE,
donateRE,
createStripeSession
];
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {

View File

@ -45,7 +45,7 @@ describe('request-authorization', () => {
const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$/;
const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/;
const allowedPathsList = [
authRE,
@ -77,9 +77,11 @@ 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', () => {

View File

@ -0,0 +1,15 @@
import { isEmail, isNumeric } from 'validator';
import {
durationKeysConfig,
donationOneTimeConfig,
donationSubscriptionConfig
} from '../../../../config/donation-settings';
export function validStripeForm(amount, duration, email) {
return isEmail('' + email) &&
isNumeric('' + amount) &&
durationKeysConfig.includes(duration) &&
duration === 'onetime'
? donationOneTimeConfig.includes(amount)
: donationSubscriptionConfig.plans[duration];
}