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=",
|
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||||
"dev": true
|
"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": {
|
"strong-error-handler": {
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-3.5.0.tgz",
|
"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",
|
"passport-mock-strategy": "^2.0.0",
|
||||||
"query-string": "^6.14.0",
|
"query-string": "^6.14.0",
|
||||||
"rx": "^4.1.0",
|
"rx": "^4.1.0",
|
||||||
|
"stripe": "^8.168.0",
|
||||||
"uuid": "^3.4.0",
|
"uuid": "^3.4.0",
|
||||||
"validator": "^9.4.1"
|
"validator": "^9.4.1"
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import { donationSubscriptionConfig } from '../../../../config/donation-settings';
|
||||||
import keys from '../../../../config/secrets';
|
import keys from '../../../../config/secrets';
|
||||||
import {
|
import {
|
||||||
getAsyncPaypalToken,
|
getAsyncPaypalToken,
|
||||||
@ -6,17 +8,139 @@ import {
|
|||||||
updateUser,
|
updateUser,
|
||||||
verifyWebHookType
|
verifyWebHookType
|
||||||
} from '../utils/donation';
|
} from '../utils/donation';
|
||||||
|
import { validStripeForm } from '../utils/stripeHelpers';
|
||||||
|
|
||||||
const log = debug('fcc:boot:donate');
|
const log = debug('fcc:boot:donate');
|
||||||
|
|
||||||
export default function donateBoot(app, done) {
|
export default function donateBoot(app, done) {
|
||||||
|
let stripe = false;
|
||||||
|
const { User } = app.models;
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const hooks = app.loopback.Router();
|
const hooks = app.loopback.Router();
|
||||||
const donateRouter = 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 { 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) {
|
if (!user || !body) {
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
@ -52,28 +176,37 @@ export default function donateBoot(app, done) {
|
|||||||
.finally(() => res.status(200).json({ message: 'received paypal hook' }));
|
.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 paypalKey = keys.paypal.client;
|
||||||
const paypalSec = keys.paypal.secret;
|
const paypalSec = keys.paypal.secret;
|
||||||
|
|
||||||
|
const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard';
|
||||||
|
const stripPublicInvalid =
|
||||||
|
!stripeKey || stripeKey === 'pk_from_stripe_dashboard';
|
||||||
|
|
||||||
const paypalSecretInvalid =
|
const paypalSecretInvalid =
|
||||||
!paypalKey || paypalKey === 'id_from_paypal_dashboard';
|
!paypalKey || paypalKey === 'id_from_paypal_dashboard';
|
||||||
const paypalPublicInvalid =
|
const paypalPublicInvalid =
|
||||||
!paypalSec || paypalSec === 'secret_from_paypal_dashboard';
|
!paypalSec || paypalSec === 'secret_from_paypal_dashboard';
|
||||||
|
|
||||||
|
const stripeInvalid = stripeSecretInvalid || stripPublicInvalid;
|
||||||
const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid;
|
const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid;
|
||||||
|
|
||||||
if (paypalInvalid) {
|
if (stripeInvalid || paypalInvalid) {
|
||||||
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
||||||
throw new Error('Donation API keys are required to boot the server!');
|
throw new Error('Donation API keys are required to boot the server!');
|
||||||
}
|
}
|
||||||
log('Donation disabled in development unless ALL test keys are provided');
|
log('Donation disabled in development unless ALL test keys are provided');
|
||||||
done();
|
done();
|
||||||
} else {
|
} else {
|
||||||
|
api.post('/charge-stripe', createStripeDonation);
|
||||||
api.post('/add-donation', addDonation);
|
api.post('/add-donation', addDonation);
|
||||||
hooks.post('/update-paypal', updatePaypal);
|
hooks.post('/update-paypal', updatePaypal);
|
||||||
donateRouter.use('/donate', api);
|
donateRouter.use('/donate', api);
|
||||||
donateRouter.use('/hooks', hooks);
|
donateRouter.use('/hooks', hooks);
|
||||||
app.use(donateRouter);
|
app.use(donateRouter);
|
||||||
|
connectToStripe(stripe).then(done);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,9 @@ export default function getCsurf() {
|
|||||||
const { path } = req;
|
const { path } = req;
|
||||||
if (
|
if (
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/^\/hooks\/update-paypal$/.test(path)
|
/^\/hooks\/update-paypal$|^\/hooks\/update-stripe$|^\/donate\/charge-stripe$/.test(
|
||||||
|
path
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,7 +23,10 @@ const signinRE = /^\/signin/;
|
|||||||
const statusRE = /^\/status\/ping$/;
|
const statusRE = /^\/status\/ping$/;
|
||||||
const unsubscribedRE = /^\/unsubscribed\//;
|
const unsubscribedRE = /^\/unsubscribed\//;
|
||||||
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
|
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 = [
|
const _pathsAllowedREs = [
|
||||||
authRE,
|
authRE,
|
||||||
@ -37,7 +40,9 @@ const _pathsAllowedREs = [
|
|||||||
statusRE,
|
statusRE,
|
||||||
unsubscribedRE,
|
unsubscribedRE,
|
||||||
unsubscribeRE,
|
unsubscribeRE,
|
||||||
updateHooksRE
|
updateHooksRE,
|
||||||
|
donateRE,
|
||||||
|
createStripeSession
|
||||||
];
|
];
|
||||||
|
|
||||||
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {
|
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {
|
||||||
|
@ -45,7 +45,7 @@ describe('request-authorization', () => {
|
|||||||
const statusRE = /^\/status\/ping$/;
|
const statusRE = /^\/status\/ping$/;
|
||||||
const unsubscribedRE = /^\/unsubscribed\//;
|
const unsubscribedRE = /^\/unsubscribed\//;
|
||||||
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
|
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
|
||||||
const updateHooksRE = /^\/hooks\/update-paypal$/;
|
const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/;
|
||||||
|
|
||||||
const allowedPathsList = [
|
const allowedPathsList = [
|
||||||
authRE,
|
authRE,
|
||||||
@ -77,9 +77,11 @@ describe('request-authorization', () => {
|
|||||||
allowedPathsList
|
allowedPathsList
|
||||||
);
|
);
|
||||||
const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList);
|
const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList);
|
||||||
|
const resultD = isAllowedPath('/hooks/update-stripe', allowedPathsList);
|
||||||
expect(resultA).toBe(true);
|
expect(resultA).toBe(true);
|
||||||
expect(resultB).toBe(true);
|
expect(resultB).toBe(true);
|
||||||
expect(resultC).toBe(true);
|
expect(resultC).toBe(true);
|
||||||
|
expect(resultD).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false for a non-white-listed path', () => {
|
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];
|
||||||
|
}
|
@ -11,6 +11,7 @@ ARG CLIENT_LOCALE
|
|||||||
ARG CURRICULUM_LOCALE
|
ARG CURRICULUM_LOCALE
|
||||||
ARG ALGOLIA_APP_ID
|
ARG ALGOLIA_APP_ID
|
||||||
ARG ALGOLIA_API_KEY
|
ARG ALGOLIA_API_KEY
|
||||||
|
ARG STRIPE_PUBLIC_KEY
|
||||||
ARG PAYPAL_CLIENT_ID
|
ARG PAYPAL_CLIENT_ID
|
||||||
ARG DEPLOYMENT_ENV
|
ARG DEPLOYMENT_ENV
|
||||||
ARG SHOW_UPCOMING_CHANGES
|
ARG SHOW_UPCOMING_CHANGES
|
||||||
|
@ -53,6 +53,16 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!env.stripePublicKey) {
|
||||||
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
||||||
|
throw new Error('Stripe public key is required to start the client!');
|
||||||
|
} else {
|
||||||
|
reporter.info(
|
||||||
|
'Stripe public key is missing or invalid. Required for Stripe integration.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { createPage } = actions;
|
const { createPage } = actions;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -307,6 +307,8 @@
|
|||||||
"confirm-2": "Confirm your one-time donation of ${{usd}}",
|
"confirm-2": "Confirm your one-time donation of ${{usd}}",
|
||||||
"confirm-3": "Confirm your donation of ${{usd}} / month",
|
"confirm-3": "Confirm your donation of ${{usd}} / month",
|
||||||
"confirm-4": "Confirm your donation of ${{usd}} / year",
|
"confirm-4": "Confirm your donation of ${{usd}} / year",
|
||||||
|
"wallet-label": "${{usd}} donation to freeCodeCamp",
|
||||||
|
"wallet-label-1": "${{usd}} / month donation to freeCodeCamp",
|
||||||
"your-donation": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world.",
|
"your-donation": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world.",
|
||||||
"your-donation-2": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world each month.",
|
"your-donation-2": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world each month.",
|
||||||
"your-donation-3": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world each year.",
|
"your-donation-3": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world each year.",
|
||||||
@ -444,6 +446,7 @@
|
|||||||
"cert-claim-success": "@{{username}}, you have successfully claimed the {{name}} Certification! Congratulations on behalf of the freeCodeCamp.org team!",
|
"cert-claim-success": "@{{username}}, you have successfully claimed the {{name}} Certification! Congratulations on behalf of the freeCodeCamp.org team!",
|
||||||
"wrong-name": "Something went wrong with the verification of {{name}}, please try again. If you continue to receive this error, you can send a message to support@freeCodeCamp.org to get help.",
|
"wrong-name": "Something went wrong with the verification of {{name}}, please try again. If you continue to receive this error, you can send a message to support@freeCodeCamp.org to get help.",
|
||||||
"error-claiming": "Error claiming {{certName}}",
|
"error-claiming": "Error claiming {{certName}}",
|
||||||
|
"refresh-needed": "You can only use the PaymentRequest button once. Refresh the page to start over.",
|
||||||
"username-not-found": "We could not find a user with the username \"{{username}}\"",
|
"username-not-found": "We could not find a user with the username \"{{username}}\"",
|
||||||
"add-name": "This user needs to add their name to their account in order for others to be able to view their certification.",
|
"add-name": "This user needs to add their name to their account in order for others to be able to view their certification.",
|
||||||
"not-eligible": "This user is not eligible for freeCodeCamp.org certifications at this time.",
|
"not-eligible": "This user is not eligible for freeCodeCamp.org certifications at this time.",
|
||||||
|
13
client/package-lock.json
generated
13
client/package-lock.json
generated
@ -3977,6 +3977,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@stripe/react-stripe-js": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-FjcVrhf72+9fUL3Lz3xi02ni9tzH1A1x6elXlr6tvBDgSD55oPJuodoP8eC7xTnBIKq0olF5uJvgtkJyDCdzjA==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@stripe/stripe-js": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-ZSHbiwTrISoaTbpercmYGuY7QTg7HxfFyNgbJBaYbwHWbzMhpEdGTsmMpaBXIU6iiqwEEDaIyD8O6yJ+H5DWCg=="
|
||||||
|
},
|
||||||
"@szmarczak/http-timer": {
|
"@szmarczak/http-timer": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
||||||
|
@ -53,7 +53,9 @@
|
|||||||
"@freecodecamp/strip-comments": "3.0.1",
|
"@freecodecamp/strip-comments": "3.0.1",
|
||||||
"@loadable/component": "5.15.0",
|
"@loadable/component": "5.15.0",
|
||||||
"@reach/router": "1.3.4",
|
"@reach/router": "1.3.4",
|
||||||
"@types/react-scrollable-anchor": "0.6.1",
|
"@stripe/react-stripe-js": "^1.4.1",
|
||||||
|
"@stripe/stripe-js": "^1.16.0",
|
||||||
|
"@types/react-scrollable-anchor": "^0.6.1",
|
||||||
"algoliasearch": "4.10.3",
|
"algoliasearch": "4.10.3",
|
||||||
"assert": "2.0.0",
|
"assert": "2.0.0",
|
||||||
"babel-plugin-preval": "5.0.0",
|
"babel-plugin-preval": "5.0.0",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
|
||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import {
|
import {
|
||||||
Col,
|
Col,
|
||||||
@ -11,6 +10,8 @@ import {
|
|||||||
ToggleButton,
|
ToggleButton,
|
||||||
ToggleButtonGroup
|
ToggleButtonGroup
|
||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
|
import type { Token } from '@stripe/stripe-js';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@ -30,7 +31,8 @@ import {
|
|||||||
addDonation,
|
addDonation,
|
||||||
updateDonationFormState,
|
updateDonationFormState,
|
||||||
defaultDonationFormState,
|
defaultDonationFormState,
|
||||||
userSelector
|
userSelector,
|
||||||
|
postChargeStripe
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
import Spacer from '../helpers/spacer';
|
import Spacer from '../helpers/spacer';
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ import DonateCompletion from './DonateCompletion';
|
|||||||
|
|
||||||
import type { AddDonationData } from './PaypalButton';
|
import type { AddDonationData } from './PaypalButton';
|
||||||
import PaypalButton from './PaypalButton';
|
import PaypalButton from './PaypalButton';
|
||||||
|
import WalletsWrapper from './walletsButton';
|
||||||
|
|
||||||
import './Donation.css';
|
import './Donation.css';
|
||||||
|
|
||||||
@ -55,6 +58,7 @@ type DonateFormState = {
|
|||||||
|
|
||||||
type DonateFormProps = {
|
type DonateFormProps = {
|
||||||
addDonation: (data: unknown) => unknown;
|
addDonation: (data: unknown) => unknown;
|
||||||
|
postChargeStripe: (data: unknown) => unknown;
|
||||||
defaultTheme?: string;
|
defaultTheme?: string;
|
||||||
email: string;
|
email: string;
|
||||||
handleProcessing: (duration: string, amount: number, action: string) => void;
|
handleProcessing: (duration: string, amount: number, action: string) => void;
|
||||||
@ -91,7 +95,8 @@ const mapStateToProps = createSelector(
|
|||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
addDonation,
|
addDonation,
|
||||||
updateDonationFormState
|
updateDonationFormState,
|
||||||
|
postChargeStripe
|
||||||
};
|
};
|
||||||
|
|
||||||
class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||||
@ -126,6 +131,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
this.handleSelectDuration = this.handleSelectDuration.bind(this);
|
this.handleSelectDuration = this.handleSelectDuration.bind(this);
|
||||||
this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this);
|
this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this);
|
||||||
this.resetDonation = this.resetDonation.bind(this);
|
this.resetDonation = this.resetDonation.bind(this);
|
||||||
|
this.postStripeDonation = this.postStripeDonation.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -180,6 +186,28 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
this.setState({ donationDuration, donationAmount });
|
this.setState({ donationDuration, donationAmount });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postStripeDonation(
|
||||||
|
token: Token,
|
||||||
|
payerEmail: string | undefined,
|
||||||
|
payerName: string | undefined
|
||||||
|
) {
|
||||||
|
const { donationAmount: amount, donationDuration: duration } = this.state;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
|
// change the donation modal button label to close
|
||||||
|
// or display the close button for the cert donation section
|
||||||
|
if (this.props.handleProcessing) {
|
||||||
|
this.props.handleProcessing(duration, amount, 'Stripe payment submition');
|
||||||
|
}
|
||||||
|
this.props.postChargeStripe({
|
||||||
|
token,
|
||||||
|
amount,
|
||||||
|
duration,
|
||||||
|
email: payerEmail,
|
||||||
|
name: payerName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
handleSelectAmount(donationAmount: number) {
|
handleSelectAmount(donationAmount: number) {
|
||||||
this.setState({ donationAmount });
|
this.setState({ donationAmount });
|
||||||
}
|
}
|
||||||
@ -277,20 +305,31 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
theme
|
theme
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { donationAmount, donationDuration } = this.state;
|
const { donationAmount, donationDuration } = this.state;
|
||||||
|
|
||||||
const isOneTime = donationDuration === 'onetime';
|
const isOneTime = donationDuration === 'onetime';
|
||||||
|
const formlabel = `${t(
|
||||||
|
isOneTime ? 'donate.confirm-2' : 'donate.confirm-3',
|
||||||
|
{ usd: donationAmount / 100 }
|
||||||
|
)}:`;
|
||||||
|
|
||||||
|
const walletlabel = `${t(
|
||||||
|
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
||||||
|
{ usd: donationAmount / 100 }
|
||||||
|
)}:`;
|
||||||
|
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isOneTime ? (
|
<b>{formlabel}</b>
|
||||||
<b>
|
|
||||||
{t('donate.confirm-1')} {donationAmount / 100}:
|
|
||||||
</b>
|
|
||||||
) : (
|
|
||||||
<b>{t('donate.confirm-3', { usd: donationAmount / 100 })}:</b>
|
|
||||||
)}
|
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<div className='donate-btn-group'>
|
<div className='donate-btn-group'>
|
||||||
|
<WalletsWrapper
|
||||||
|
amount={donationAmount}
|
||||||
|
label={walletlabel}
|
||||||
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
|
postStripeDonation={this.postStripeDonation}
|
||||||
|
refreshErrorMessage={t('donate.refresh-needed')}
|
||||||
|
theme={priorityTheme}
|
||||||
|
/>
|
||||||
<PaypalButton
|
<PaypalButton
|
||||||
addDonation={addDonation}
|
addDonation={addDonation}
|
||||||
donationAmount={donationAmount}
|
donationAmount={donationAmount}
|
||||||
@ -299,7 +338,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
isSubscription={isOneTime ? false : true}
|
isSubscription={isOneTime ? false : true}
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
skipAddDonation={!isSignedIn}
|
skipAddDonation={!isSignedIn}
|
||||||
theme={defaultTheme ? defaultTheme : theme}
|
theme={priorityTheme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -322,13 +361,28 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
|
|
||||||
renderModalForm() {
|
renderModalForm() {
|
||||||
const { donationAmount, donationDuration } = this.state;
|
const { donationAmount, donationDuration } = this.state;
|
||||||
const { handleProcessing, addDonation, defaultTheme, theme } = this.props;
|
const { handleProcessing, addDonation, defaultTheme, theme, t } =
|
||||||
|
this.props;
|
||||||
|
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
||||||
|
const isOneTime = donationDuration === 'onetime';
|
||||||
|
const walletlabel = `${t(
|
||||||
|
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
||||||
|
{ usd: donationAmount / 100 }
|
||||||
|
)}:`;
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
<b className='donation-label'>{this.getDonationButtonLabel()}:</b>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<b>{this.getDonationButtonLabel()}:</b>
|
<div className='donate-btn-group'>
|
||||||
<Spacer />
|
<WalletsWrapper
|
||||||
|
amount={donationAmount}
|
||||||
|
label={walletlabel}
|
||||||
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
|
postStripeDonation={this.postStripeDonation}
|
||||||
|
refreshErrorMessage={t('donate.refresh-needed')}
|
||||||
|
theme={priorityTheme}
|
||||||
|
/>
|
||||||
<PaypalButton
|
<PaypalButton
|
||||||
addDonation={addDonation}
|
addDonation={addDonation}
|
||||||
donationAmount={donationAmount}
|
donationAmount={donationAmount}
|
||||||
@ -337,6 +391,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
onDonationStateChange={this.onDonationStateChange}
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
theme={defaultTheme ? defaultTheme : theme}
|
theme={defaultTheme ? defaultTheme : theme}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
@ -245,11 +245,13 @@ li.disabled > a {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
.donation-label,
|
||||||
.donation-modal p,
|
.donation-modal p,
|
||||||
.donation-modal b {
|
.donation-modal b {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-icon-container {
|
.donation-icon-container {
|
||||||
@ -386,31 +388,23 @@ button#confirm-donation-btn:hover {
|
|||||||
|
|
||||||
.donate-btn-group {
|
.donate-btn-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-btn-group > * {
|
.donate-btn-group > * {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 43px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-btn-group button:first-child {
|
.wallets-form {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 500px) {
|
@media (min-width: 500px) {
|
||||||
.donate-btn-group {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.donate-btn-group > * {
|
.donate-btn-group > * {
|
||||||
width: 49%;
|
width: 49%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-btn-group button:first-child {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.donate-page-wrapper .alert.alert-info a:hover {
|
.donate-page-wrapper .alert.alert-info a:hover {
|
||||||
|
131
client/src/components/Donation/walletsButton.tsx
Normal file
131
client/src/components/Donation/walletsButton.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
PaymentRequestButtonElement,
|
||||||
|
Elements,
|
||||||
|
ElementsConsumer
|
||||||
|
} from '@stripe/react-stripe-js';
|
||||||
|
import { Stripe, loadStripe } from '@stripe/stripe-js';
|
||||||
|
import type { Token, PaymentRequest } from '@stripe/stripe-js';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import envData from '../../../../config/env.json';
|
||||||
|
import { AddDonationData } from './PaypalButton';
|
||||||
|
|
||||||
|
const { stripePublicKey }: { stripePublicKey: string | null } = envData;
|
||||||
|
|
||||||
|
interface WrapperProps {
|
||||||
|
label: string;
|
||||||
|
amount: number;
|
||||||
|
theme: string;
|
||||||
|
postStripeDonation: (
|
||||||
|
token: Token,
|
||||||
|
payerEmail: string | undefined,
|
||||||
|
payerName: string | undefined
|
||||||
|
) => void;
|
||||||
|
onDonationStateChange: (donationState: AddDonationData) => void;
|
||||||
|
refreshErrorMessage: string;
|
||||||
|
}
|
||||||
|
interface WalletsButtonProps extends WrapperProps {
|
||||||
|
stripe: Stripe | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WalletsButton = ({
|
||||||
|
stripe,
|
||||||
|
label,
|
||||||
|
amount,
|
||||||
|
theme,
|
||||||
|
refreshErrorMessage,
|
||||||
|
postStripeDonation,
|
||||||
|
onDonationStateChange
|
||||||
|
}: WalletsButtonProps) => {
|
||||||
|
const [token, setToken] = useState<Token | null>(null);
|
||||||
|
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [canMakePayment, checkpaymentPossiblity] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stripe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = stripe.paymentRequest({
|
||||||
|
country: 'US',
|
||||||
|
currency: 'usd',
|
||||||
|
total: { label, amount },
|
||||||
|
requestPayerName: true,
|
||||||
|
requestPayerEmail: true,
|
||||||
|
disableWallets: ['browserCard']
|
||||||
|
});
|
||||||
|
|
||||||
|
pr.on('token', event => {
|
||||||
|
const { token, payerEmail, payerName } = event;
|
||||||
|
setToken(token);
|
||||||
|
event.complete('success');
|
||||||
|
postStripeDonation(token, payerEmail, payerName);
|
||||||
|
});
|
||||||
|
|
||||||
|
void pr.canMakePayment().then(canMakePaymentRes => {
|
||||||
|
if (canMakePaymentRes) {
|
||||||
|
setPaymentRequest(pr);
|
||||||
|
checkpaymentPossiblity(true);
|
||||||
|
} else {
|
||||||
|
checkpaymentPossiblity(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [label, amount, stripe, postStripeDonation]);
|
||||||
|
|
||||||
|
const displayRefreshError = (): void => {
|
||||||
|
onDonationStateChange({
|
||||||
|
redirecting: false,
|
||||||
|
processing: false,
|
||||||
|
success: false,
|
||||||
|
error: refreshErrorMessage
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className='wallets-form'>
|
||||||
|
{canMakePayment && paymentRequest && (
|
||||||
|
<PaymentRequestButtonElement
|
||||||
|
onClick={() => {
|
||||||
|
if (token) {
|
||||||
|
displayRefreshError();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
style: {
|
||||||
|
paymentRequestButton: {
|
||||||
|
type: 'default',
|
||||||
|
theme: theme === 'night' ? 'light' : 'dark',
|
||||||
|
height: '43px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
paymentRequest
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InjectedCheckoutForm = (props: WrapperProps): JSX.Element => (
|
||||||
|
<ElementsConsumer>
|
||||||
|
{({ stripe }: { stripe: Stripe | null }) => (
|
||||||
|
<WalletsButton stripe={stripe} {...props} />
|
||||||
|
)}
|
||||||
|
</ElementsConsumer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const WalletsWrapper = (props: WrapperProps): JSX.Element | null => {
|
||||||
|
if (!stripePublicKey) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
const stripePromise = loadStripe(stripePublicKey);
|
||||||
|
return (
|
||||||
|
<Elements stripe={stripePromise}>
|
||||||
|
<InjectedCheckoutForm {...props} />
|
||||||
|
</Elements>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WalletsWrapper;
|
@ -27,7 +27,8 @@ export const actionTypes = createTypes(
|
|||||||
...createAsyncTypes('fetchProfileForUser'),
|
...createAsyncTypes('fetchProfileForUser'),
|
||||||
...createAsyncTypes('acceptTerms'),
|
...createAsyncTypes('acceptTerms'),
|
||||||
...createAsyncTypes('showCert'),
|
...createAsyncTypes('showCert'),
|
||||||
...createAsyncTypes('reportUser')
|
...createAsyncTypes('reportUser'),
|
||||||
|
...createAsyncTypes('postChargeStripe')
|
||||||
],
|
],
|
||||||
ns
|
ns
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
import { put, select, takeEvery, delay, call, take } from 'redux-saga/effects';
|
import {
|
||||||
import { addDonation } from '../utils/ajax';
|
put,
|
||||||
|
select,
|
||||||
|
takeEvery,
|
||||||
|
takeLeading,
|
||||||
|
delay,
|
||||||
|
call,
|
||||||
|
take
|
||||||
|
} from 'redux-saga/effects';
|
||||||
|
import { addDonation, postChargeStripe } from '../utils/ajax';
|
||||||
import { actionTypes as appTypes } from './action-types';
|
import { actionTypes as appTypes } from './action-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -9,7 +17,9 @@ import {
|
|||||||
preventProgressDonationRequests,
|
preventProgressDonationRequests,
|
||||||
recentlyClaimedBlockSelector,
|
recentlyClaimedBlockSelector,
|
||||||
addDonationComplete,
|
addDonationComplete,
|
||||||
addDonationError
|
addDonationError,
|
||||||
|
postChargeStripeComplete,
|
||||||
|
postChargeStripeError
|
||||||
} from './';
|
} from './';
|
||||||
|
|
||||||
const defaultDonationError = `Something is not right. Please contact donors@freecodecamp.org`;
|
const defaultDonationError = `Something is not right. Please contact donors@freecodecamp.org`;
|
||||||
@ -44,9 +54,23 @@ function* addDonationSaga({ payload }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* postChargeStripeSaga({ payload }) {
|
||||||
|
try {
|
||||||
|
yield call(postChargeStripe, payload);
|
||||||
|
yield put(postChargeStripeComplete());
|
||||||
|
} catch (error) {
|
||||||
|
const err =
|
||||||
|
error.response && error.response.data
|
||||||
|
? error.response.data.error
|
||||||
|
: defaultDonationError;
|
||||||
|
yield put(postChargeStripeError(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createDonationSaga(types) {
|
export function createDonationSaga(types) {
|
||||||
return [
|
return [
|
||||||
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
|
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
|
||||||
takeEvery(types.addDonation, addDonationSaga)
|
takeEvery(types.addDonation, addDonationSaga),
|
||||||
|
takeLeading(types.postChargeStripe, postChargeStripeSaga)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -125,6 +125,14 @@ export const addDonationComplete = createAction(
|
|||||||
);
|
);
|
||||||
export const addDonationError = createAction(actionTypes.addDonationError);
|
export const addDonationError = createAction(actionTypes.addDonationError);
|
||||||
|
|
||||||
|
export const postChargeStripe = createAction(actionTypes.postChargeStripe);
|
||||||
|
export const postChargeStripeComplete = createAction(
|
||||||
|
actionTypes.postChargeStripeComplete
|
||||||
|
);
|
||||||
|
export const postChargeStripeError = createAction(
|
||||||
|
actionTypes.postChargeStripeError
|
||||||
|
);
|
||||||
|
|
||||||
export const fetchProfileForUser = createAction(
|
export const fetchProfileForUser = createAction(
|
||||||
actionTypes.fetchProfileForUser
|
actionTypes.fetchProfileForUser
|
||||||
);
|
);
|
||||||
@ -415,6 +423,29 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
donationFormState: { ...defaultDonationFormState, error: payload }
|
donationFormState: { ...defaultDonationFormState, error: payload }
|
||||||
}),
|
}),
|
||||||
|
[actionTypes.postChargeStripe]: state => ({
|
||||||
|
...state,
|
||||||
|
donationFormState: { ...defaultDonationFormState, processing: true }
|
||||||
|
}),
|
||||||
|
[actionTypes.postChargeStripeComplete]: state => {
|
||||||
|
const { appUsername } = state;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[appUsername]: {
|
||||||
|
...state.user[appUsername],
|
||||||
|
isDonating: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
donationFormState: { ...defaultDonationFormState, success: true }
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[actionTypes.postChargeStripeError]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
donationFormState: { ...defaultDonationFormState, error: payload }
|
||||||
|
}),
|
||||||
[actionTypes.fetchUser]: state => ({
|
[actionTypes.fetchUser]: state => ({
|
||||||
...state,
|
...state,
|
||||||
userFetchState: { ...defaultFetchState }
|
userFetchState: { ...defaultFetchState }
|
||||||
|
@ -104,6 +104,9 @@ export function addDonation(body: Donation): Promise<void> {
|
|||||||
return post('/donate/add-donation', body);
|
return post('/donate/add-donation', body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function postChargeStripe(body: Donation): Promise<void> {
|
||||||
|
return post('/donate/charge-stripe', body);
|
||||||
|
}
|
||||||
interface Report {
|
interface Report {
|
||||||
username: string;
|
username: string;
|
||||||
reportDescription: string;
|
reportDescription: string;
|
||||||
|
@ -23,6 +23,7 @@ const {
|
|||||||
SHOW_LOCALE_DROPDOWN_MENU: showLocaleDropdownMenu,
|
SHOW_LOCALE_DROPDOWN_MENU: showLocaleDropdownMenu,
|
||||||
ALGOLIA_APP_ID: algoliaAppId,
|
ALGOLIA_APP_ID: algoliaAppId,
|
||||||
ALGOLIA_API_KEY: algoliaAPIKey,
|
ALGOLIA_API_KEY: algoliaAPIKey,
|
||||||
|
STRIPE_PUBLIC_KEY: stripePublicKey,
|
||||||
PAYPAL_CLIENT_ID: paypalClientId,
|
PAYPAL_CLIENT_ID: paypalClientId,
|
||||||
DEPLOYMENT_ENV: deploymentEnv,
|
DEPLOYMENT_ENV: deploymentEnv,
|
||||||
SHOW_UPCOMING_CHANGES: showUpcomingChanges
|
SHOW_UPCOMING_CHANGES: showUpcomingChanges
|
||||||
@ -52,6 +53,10 @@ module.exports = Object.assign(locations, {
|
|||||||
!algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard'
|
!algoliaAPIKey || algoliaAPIKey === 'api_key_from_algolia_dashboard'
|
||||||
? ''
|
? ''
|
||||||
: algoliaAPIKey,
|
: algoliaAPIKey,
|
||||||
|
stripePublicKey:
|
||||||
|
!stripePublicKey || stripePublicKey === 'pk_from_stripe_dashboard'
|
||||||
|
? null
|
||||||
|
: stripePublicKey,
|
||||||
paypalClientId:
|
paypalClientId:
|
||||||
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard'
|
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard'
|
||||||
? null
|
? null
|
||||||
|
@ -29,6 +29,9 @@ const {
|
|||||||
|
|
||||||
SENTRY_DSN,
|
SENTRY_DSN,
|
||||||
|
|
||||||
|
STRIPE_PUBLIC_KEY,
|
||||||
|
STRIPE_SECRET_KEY,
|
||||||
|
|
||||||
PAYPAL_CLIENT_ID,
|
PAYPAL_CLIENT_ID,
|
||||||
PAYPAL_SECRET,
|
PAYPAL_SECRET,
|
||||||
PAYPAL_VERIFY_WEBHOOK_URL,
|
PAYPAL_VERIFY_WEBHOOK_URL,
|
||||||
@ -92,6 +95,11 @@ module.exports = {
|
|||||||
dns: SENTRY_DSN
|
dns: SENTRY_DSN
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stripe: {
|
||||||
|
public: STRIPE_PUBLIC_KEY,
|
||||||
|
secret: STRIPE_SECRET_KEY
|
||||||
|
},
|
||||||
|
|
||||||
paypal: {
|
paypal: {
|
||||||
client: PAYPAL_CLIENT_ID,
|
client: PAYPAL_CLIENT_ID,
|
||||||
secret: PAYPAL_SECRET,
|
secret: PAYPAL_SECRET,
|
||||||
|
@ -31,6 +31,11 @@ ALGOLIA_API_KEY=api_key_from_algolia_dashboard
|
|||||||
# Donations
|
# Donations
|
||||||
# ---------------------
|
# ---------------------
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
STRIPE_CREATE_PLANS=true
|
||||||
|
STRIPE_PUBLIC_KEY=pk_from_stripe_dashboard
|
||||||
|
STRIPE_SECRET_KEY=sk_from_stripe_dashboard
|
||||||
|
|
||||||
# PayPal
|
# PayPal
|
||||||
PAYPAL_CLIENT_ID=id_from_paypal_dashboard
|
PAYPAL_CLIENT_ID=id_from_paypal_dashboard
|
||||||
PAYPAL_SECRET=secret_from_paypal_dashboard
|
PAYPAL_SECRET=secret_from_paypal_dashboard
|
||||||
|
@ -45,7 +45,7 @@ if (FREECODECAMP_NODE_ENV !== 'development') {
|
|||||||
'showUpcomingChanges'
|
'showUpcomingChanges'
|
||||||
];
|
];
|
||||||
const searchKeys = ['algoliaAppId', 'algoliaAPIKey'];
|
const searchKeys = ['algoliaAppId', 'algoliaAPIKey'];
|
||||||
const donationKeys = ['paypalClientId'];
|
const donationKeys = ['stripePublicKey', 'paypalClientId'];
|
||||||
|
|
||||||
const expectedVariables = locationKeys.concat(
|
const expectedVariables = locationKeys.concat(
|
||||||
deploymentKeys,
|
deploymentKeys,
|
||||||
|
Reference in New Issue
Block a user