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