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:
16
api-server/package-lock.json
generated
16
api-server/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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', () => {
|
||||
|
15
api-server/src/server/utils/stripeHelpers.js
Normal file
15
api-server/src/server/utils/stripeHelpers.js
Normal 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];
|
||||
}
|
Reference in New Issue
Block a user