diff --git a/api-server/package-lock.json b/api-server/package-lock.json index a56bc8fd41..bb18999642 100644 --- a/api-server/package-lock.json +++ b/api-server/package-lock.json @@ -16574,24 +16574,6 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, - "stripe": { - "version": "6.36.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-6.36.0.tgz", - "integrity": "sha512-7vjyVO4NCWvX38CH1AuSQH16uRxcQN+UhUTBPs4UHsIl5+SJXLBvCsHrMgd+bY9k1YDliT0fQB1fH9OI3GrEhw==", - "requires": { - "lodash.isplainobject": "^4.0.6", - "qs": "^6.6.0", - "safe-buffer": "^5.1.1", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" - } - } - }, "strong-error-handler": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-3.5.0.tgz", diff --git a/api-server/package.json b/api-server/package.json index e04045abae..58d554be99 100644 --- a/api-server/package.json +++ b/api-server/package.json @@ -69,7 +69,6 @@ "passport-mock-strategy": "^2.0.0", "query-string": "^6.14.0", "rx": "^4.1.0", - "stripe": "^6.36.0", "uuid": "^3.4.0", "validator": "^9.4.1" }, diff --git a/api-server/src/server/boot/donate.js b/api-server/src/server/boot/donate.js index abf52df6ef..40a39b8982 100644 --- a/api-server/src/server/boot/donate.js +++ b/api-server/src/server/boot/donate.js @@ -1,231 +1,19 @@ -import Stripe from 'stripe'; import debug from 'debug'; -import { isEmail, isNumeric } from 'validator'; - import { getAsyncPaypalToken, verifyWebHook, updateUser, verifyWebHookType } from '../utils/donation'; -import { - durationKeysConfig, - donationOneTimeConfig, - donationSubscriptionConfig, - durationsConfig, - onetimeSKUConfig, - donationUrls -} from '../../../../config/donation-settings'; import keys from '../../../../config/secrets'; -import { deploymentEnv } from '../../../../config/env'; const log = debug('fcc:boot:donate'); export default function donateBoot(app, done) { - let stripe = false; - const { User } = app.models; const api = app.loopback.Router(); const hooks = app.loopback.Router(); const donateRouter = app.loopback.Router(); - const subscriptionPlans = Object.keys( - donationSubscriptionConfig.plans - ).reduce( - (prevDuration, duration) => - prevDuration.concat( - donationSubscriptionConfig.plans[duration].reduce( - (prevAmount, amount) => - prevAmount.concat({ - amount: amount, - interval: duration, - product: { - name: `${ - donationSubscriptionConfig.duration[duration] - } Donation to freeCodeCamp.org - Thank you ($${amount / 100})`, - metadata: { - /* eslint-disable camelcase */ - sb_service: `freeCodeCamp.org`, - sb_tier: `${donationSubscriptionConfig.duration[duration]} $${ - amount / 100 - } Donation` - /* eslint-enable camelcase */ - } - }, - currency: 'usd', - id: `${donationSubscriptionConfig.duration[ - duration - ].toLowerCase()}-donation-${amount}` - }), - [] - ) - ), - [] - ); - - function validStripeForm(amount, duration, email) { - return isEmail('' + email) && - isNumeric('' + amount) && - durationKeysConfig.includes(duration) && - duration === 'onetime' - ? donationOneTimeConfig.includes(amount) - : donationSubscriptionConfig.plans[duration]; - } - - function connectToStripe() { - return new Promise(function (resolve) { - // connect to stripe API - stripe = Stripe(keys.stripe.secret); - // parse stripe plans - stripe.plans.list({}, function (err, stripePlans) { - if (err) { - throw err; - } - const requiredPlans = subscriptionPlans.map(plan => plan.id); - const availablePlans = stripePlans.data.map(plan => plan.id); - if (process.env.STRIPE_CREATE_PLANS === 'true') { - requiredPlans.forEach(requiredPlan => { - if (!availablePlans.includes(requiredPlan)) { - createStripePlan( - subscriptionPlans.find(plan => plan.id === requiredPlan) - ); - } - }); - } else { - log(`Skipping plan creation`); - } - }); - resolve(); - }); - } - - function createStripePlan(plan) { - log(`Creating subscription plan: ${plan.product.name}`); - stripe.plans.create(plan, function (err) { - if (err) { - log(err); - } - log(`Created plan with plan id: ${plan.id}`); - return; - }); - } - - function createStripeDonation(req, res) { - const { user, body } = req; - - const { - amount, - duration, - token: { email, id } - } = body; - - if (!validStripeForm(amount, duration, email)) { - return res.status(500).send({ - error: 'The donation form had invalid values for this submission.' - }); - } - - const fccUser = user - ? Promise.resolve(user) - : new Promise((resolve, reject) => - User.findOrCreate( - { where: { email } }, - { email }, - (err, instance, isNew) => { - log('createing a new donating user instance: ', isNew); - if (err) { - return reject(err); - } - return resolve(instance); - } - ) - ); - - let donatingUser = {}; - let donation = { - email, - amount, - duration, - provider: 'stripe', - startDate: new Date(Date.now()).toISOString() - }; - - const createCustomer = user => { - donatingUser = user; - return stripe.customers.create({ - email, - card: id - }); - }; - - const createSubscription = customer => { - donation.customerId = customer.id; - return stripe.subscriptions.create({ - customer: customer.id, - items: [ - { - plan: `${donationSubscriptionConfig.duration[ - duration - ].toLowerCase()}-donation-${amount}` - } - ] - }); - }; - - const createOneTimeCharge = customer => { - donation.customerId = customer.id; - return stripe.charges.create({ - amount: amount, - currency: 'usd', - customer: customer.id - }); - }; - - const createAsyncUserDonation = () => { - donatingUser - .createDonation(donation) - .toPromise() - .catch(err => { - throw new Error(err); - }); - }; - - return Promise.resolve(fccUser) - .then(nonDonatingUser => { - const { isDonating } = nonDonatingUser; - if (isDonating && duration !== 'onetime') { - throw { - message: `User already has active recurring donation(s).`, - type: 'AlreadyDonatingError' - }; - } - return nonDonatingUser; - }) - .then(createCustomer) - .then(customer => { - return duration === 'onetime' - ? createOneTimeCharge(customer).then(charge => { - donation.subscriptionId = 'one-time-charge-prefix-' + charge.id; - return res.send(charge); - }) - : createSubscription(customer).then(subscription => { - donation.subscriptionId = subscription.id; - return res.send(subscription); - }); - }) - .then(createAsyncUserDonation) - .catch(err => { - if ( - err.type === 'StripeCardError' || - err.type === 'AlreadyDonatingError' - ) { - return res.status(402).send({ error: err.message }); - } - return res - .status(500) - .send({ error: 'Donation failed due to a server error.' }); - }); - } - function addDonation(req, res) { const { user, body } = req; @@ -250,53 +38,6 @@ export default function donateBoot(app, done) { }); } - async function createStripeSession(req, res) { - const { - body, - body: { donationAmount, donationDuration } - } = req; - if (!body) { - return res - .status(500) - .send({ type: 'danger', message: 'Request has not completed.' }); - } - const isSubscription = donationDuration !== 'onetime'; - const getSKUId = () => { - const { id } = onetimeSKUConfig[deploymentEnv || 'staging'].find( - skuConfig => skuConfig.amount === `${donationAmount}` - ); - return id; - }; - const price = isSubscription - ? `${durationsConfig[donationDuration]}-donation-${donationAmount}` - : getSKUId(); - - /* eslint-disable camelcase */ - try { - const session = await stripe.checkout.sessions.create({ - payment_method_types: ['card'], - line_items: [ - { - price, - quantity: 1 - } - ], - metadata: { ...body }, - mode: isSubscription ? 'subscription' : 'payment', - success_url: donationUrls.successUrl, - cancel_url: donationUrls.cancelUrl - }); - /* eslint-enable camelcase */ - return res.status(200).json({ id: session.id }); - } catch (err) { - log(err.message); - return res.status(500).send({ - type: 'danger', - message: 'Something went wrong.' - }); - } - } - function updatePaypal(req, res) { const { headers, body } = req; return Promise.resolve(req) @@ -311,13 +52,8 @@ export default function donateBoot(app, done) { .finally(() => res.status(200).json({ message: 'received paypal hook' })); } - const stripeKey = keys.stripe.public; - const secKey = keys.stripe.secret; const paypalKey = keys.paypal.client; const paypalSec = keys.paypal.secret; - const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard'; - const stripPublicInvalid = - !stripeKey || stripeKey === 'pk_from_stripe_dashboard'; const paypalSecretInvalid = !paypalKey || paypalKey === 'id_from_paypal_dashboard'; @@ -325,22 +61,19 @@ export default function donateBoot(app, done) { !paypalSec || paypalSec === 'secret_from_paypal_dashboard'; const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid; - const stripeInvalid = stripeSecretInvalid || stripPublicInvalid; - if (stripeInvalid || paypalInvalid) { + if (paypalInvalid) { if (process.env.FREECODECAMP_NODE_ENV === 'production') { throw new Error('Donation API keys are required to boot the server!'); } log('Donation disabled in development unless ALL test keys are provided'); done(); } else { - api.post('/charge-stripe', createStripeDonation); - api.post('/create-stripe-session', createStripeSession); api.post('/add-donation', addDonation); hooks.post('/update-paypal', updatePaypal); donateRouter.use('/donate', api); donateRouter.use('/hooks', hooks); app.use(donateRouter); - connectToStripe().then(done); + done(); } } diff --git a/api-server/src/server/middlewares/csurf.js b/api-server/src/server/middlewares/csurf.js index ebce81abc3..9c790509c5 100644 --- a/api-server/src/server/middlewares/csurf.js +++ b/api-server/src/server/middlewares/csurf.js @@ -12,9 +12,7 @@ export default function getCsurf() { const { path } = req; if ( // eslint-disable-next-line max-len - /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$|^\/donate\/charge-stripe$/.test( - path - ) + /^\/hooks\/update-paypal$/.test(path) ) { return next(); } diff --git a/api-server/src/server/middlewares/request-authorization.js b/api-server/src/server/middlewares/request-authorization.js index 28b6367773..18e0f88289 100644 --- a/api-server/src/server/middlewares/request-authorization.js +++ b/api-server/src/server/middlewares/request-authorization.js @@ -23,11 +23,7 @@ const signinRE = /^\/signin/; const statusRE = /^\/status\/ping$/; const unsubscribedRE = /^\/unsubscribed\//; const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//; -const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/; -const createStripeSession = /^\/donate\/create-stripe-session/; - -// note: this would be replaced by webhooks later -const donateRE = /^\/donate\/charge-stripe$/; +const updateHooksRE = /^\/hooks\/update-paypal$/; const _pathsAllowedREs = [ authRE, @@ -41,9 +37,7 @@ const _pathsAllowedREs = [ statusRE, unsubscribedRE, unsubscribeRE, - updateHooksRE, - donateRE, - createStripeSession + updateHooksRE ]; export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) { diff --git a/api-server/src/server/middlewares/request-authorization.test.js b/api-server/src/server/middlewares/request-authorization.test.js index f71c43ccfa..552669f40e 100644 --- a/api-server/src/server/middlewares/request-authorization.test.js +++ b/api-server/src/server/middlewares/request-authorization.test.js @@ -47,7 +47,7 @@ describe('request-authorization', () => { const statusRE = /^\/status\/ping$/; const unsubscribedRE = /^\/unsubscribed\//; const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//; - const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/; + const updateHooksRE = /^\/hooks\/update-paypal$/; const allowedPathsList = [ authRE, @@ -79,11 +79,9 @@ describe('request-authorization', () => { allowedPathsList ); const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList); - const resultD = isAllowedPath('/hooks/update-stripe', allowedPathsList); expect(resultA).toBe(true); expect(resultB).toBe(true); expect(resultC).toBe(true); - expect(resultD).toBe(true); }); it('returns false for a non-white-listed path', () => { diff --git a/client.Dockerfile b/client.Dockerfile index 2289fcf806..5c8b4d2e1b 100644 --- a/client.Dockerfile +++ b/client.Dockerfile @@ -9,7 +9,6 @@ ARG FORUM_LOCATION ARG NEWS_LOCATION ARG CLIENT_LOCALE ARG CURRICULUM_LOCALE -ARG STRIPE_PUBLIC_KEY ARG ALGOLIA_APP_ID ARG ALGOLIA_API_KEY ARG PAYPAL_CLIENT_ID diff --git a/client/gatsby-node.js b/client/gatsby-node.js index 6ffe2d7e96..97305f548e 100644 --- a/client/gatsby-node.js +++ b/client/gatsby-node.js @@ -53,15 +53,6 @@ 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 missing or invalid. Required for donations.' - ); - } - } const { createPage } = actions; return new Promise((resolve, reject) => { @@ -186,8 +177,7 @@ exports.onCreateWebpackConfig = ({ stage, plugins, actions }) => { plugins.define({ HOME_PATH: JSON.stringify( process.env.HOME_PATH || 'http://localhost:3000' - ), - STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || '') + ) }), // We add the shims of the node globals to the global scope new webpack.ProvidePlugin({ diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 772921a8d2..15bd0852f0 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -297,6 +297,7 @@ "help-more": "Help us do more", "error": "Something went wrong with your donation.", "free-tech": "Your donations will support free technology education for people all over the world.", + "no-halo": "If you don't see a gold halo around your profile picture, contact donors@freecodecamp.org.", "gift-frequency": "Select gift frequency:", "gift-amount": "Select gift amount:", "confirm": "Confirm your donation", diff --git a/client/package-lock.json b/client/package-lock.json index 7c8b7c5627..bde293b648 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3224,14 +3224,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "@stripe/react-stripe-js": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.4.0.tgz", - "integrity": "sha512-Pz5QmG8PgJ3pi8gOWxlngk+ns63p2L1Ds192fn55ykZNRKfGz3G6sfssUVThHn/NAt2Hp1eCEsy/hvlKnXJI6g==", - "requires": { - "prop-types": "^15.7.2" - } - }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", diff --git a/client/package.json b/client/package.json index b564db4e98..f17843d9ea 100644 --- a/client/package.json +++ b/client/package.json @@ -52,7 +52,6 @@ "@freecodecamp/react-calendar-heatmap": "1.0.0", "@loadable/component": "5.14.1", "@reach/router": "1.3.4", - "@stripe/react-stripe-js": "1.4.0", "algoliasearch": "3.35.1", "assert": "2.0.0", "axios": "0.21.1", diff --git a/client/src/client-only-routes/ShowCertification.js b/client/src/client-only-routes/ShowCertification.js index e98c7af2cd..f177432832 100644 --- a/client/src/client-only-routes/ShowCertification.js +++ b/client/src/client-only-routes/ShowCertification.js @@ -168,11 +168,7 @@ const ShowCertification = props => { setIsDonationClosed(true); }; - const handleProcessing = ( - duration, - amount, - action = 'stripe form submission' - ) => { + const handleProcessing = (duration, amount, action) => { props.executeGA({ type: 'event', data: { diff --git a/client/src/components/Donation/DonateCompletion.js b/client/src/components/Donation/DonateCompletion.js index 107f74c570..1745109a4b 100644 --- a/client/src/components/Donation/DonateCompletion.js +++ b/client/src/components/Donation/DonateCompletion.js @@ -49,6 +49,7 @@ function DonateCompletion({ {success && (
{t('donate.free-tech')}
+{t('donate.no-halo')}
{error}
} diff --git a/client/src/components/Donation/DonateForm.js b/client/src/components/Donation/DonateForm.js index 3672b3742e..4b78137620 100644 --- a/client/src/components/Donation/DonateForm.js +++ b/client/src/components/Donation/DonateForm.js @@ -3,9 +3,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { Elements } from '@stripe/react-stripe-js'; import { - Button, Col, Row, Tab, @@ -20,23 +18,16 @@ import { durationsConfig, defaultAmount, defaultDonation, - donationUrls, modalDefaultDonation } from '../../../../config/donation-settings'; -import envData from '../../../../config/env.json'; -import { stripeScriptLoader } from '../../utils/scriptLoaders'; import Spacer from '../helpers/Spacer'; import PaypalButton from './PaypalButton'; import DonateCompletion from './DonateCompletion'; -import StripeCardForm from './StripeCardForm'; import { isSignedInSelector, signInLoadingSelector, donationFormStateSelector, - hardGoTo as navigate, addDonation, - createStripeSession, - postChargeStripe, updateDonationFormState, defaultDonationFormState, userSelector @@ -44,14 +35,11 @@ import { import './Donation.css'; -const { stripePublicKey } = envData; - const numToCommas = num => num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); const propTypes = { addDonation: PropTypes.func, - createStripeSession: PropTypes.func, defaultTheme: PropTypes.string, donationFormState: PropTypes.object, email: PropTypes.string, @@ -59,8 +47,6 @@ const propTypes = { isDonating: PropTypes.bool, isMinimalForm: PropTypes.bool, isSignedIn: PropTypes.bool, - navigate: PropTypes.func.isRequired, - postChargeStripe: PropTypes.func.isRequired, showLoading: PropTypes.bool.isRequired, t: PropTypes.func.isRequired, theme: PropTypes.string, @@ -83,10 +69,7 @@ const mapStateToProps = createSelector( const mapDispatchToProps = { addDonation, - navigate, - postChargeStripe, - updateDonationFormState, - createStripeSession + updateDonationFormState }; class DonateForm extends Component { @@ -102,63 +85,26 @@ class DonateForm extends Component { this.state = { ...initialAmountAndDuration, - processing: false, - stripe: null + processing: false }; - this.handleStripeLoad = this.handleStripeLoad.bind(this); this.onDonationStateChange = this.onDonationStateChange.bind(this); this.getActiveDonationAmount = this.getActiveDonationAmount.bind(this); this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this); this.handleSelectAmount = this.handleSelectAmount.bind(this); this.handleSelectDuration = this.handleSelectDuration.bind(this); - this.handleStripeCheckoutRedirect = this.handleStripeCheckoutRedirect.bind( - this - ); this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this); this.resetDonation = this.resetDonation.bind(this); - this.postStripeDonation = this.postStripeDonation.bind(this); - } - - componentDidMount() { - if (window.Stripe) { - this.handleStripeLoad(); - } else if (document.querySelector('#stripe-js')) { - document - .querySelector('#stripe-js') - .addEventListener('load', this.handleStripeLoad); - } else { - stripeScriptLoader(this.handleStripeLoad); - } } componentWillUnmount() { - const stripeMountPoint = document.querySelector('#stripe-js'); - if (stripeMountPoint) { - stripeMountPoint.removeEventListener('load', this.handleStripeLoad); - } this.resetDonation(); } - handleStripeLoad() { - // Create Stripe instance once Stripe.js loads - if (stripePublicKey) { - this.setState(state => ({ - ...state, - stripe: window.Stripe(stripePublicKey) - })); - } - } - onDonationStateChange(donationState) { // scroll to top window.scrollTo(0, 0); - this.props.updateDonationFormState(donationState); - // send donation made on the donate page to related news article - if (donationState.success && !this.props.isMinimalForm) { - this.props.navigate(donationUrls.successUrl); - } } getActiveDonationAmount(durationSelected, amountSelected) { @@ -204,41 +150,6 @@ class DonateForm extends Component { this.setState({ donationAmount }); } - postStripeDonation(token) { - 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); - } - this.props.postChargeStripe({ token, amount, duration }); - } - - async handleStripeCheckoutRedirect(e, paymentMethod) { - e.preventDefault(); - const { stripe, donationAmount, donationDuration } = this.state; - const { handleProcessing, email } = this.props; - - handleProcessing( - donationDuration, - donationAmount, - `stripe (${paymentMethod}) button click` - ); - - this.props.createStripeSession({ - stripe, - data: { - donationAmount, - donationDuration, - clickedPaymentMethod: paymentMethod, - email, - context: 'donate page' - } - }); - } - renderAmountButtons(duration) { return this.amounts[duration].map(amount => ({t('donate.valid-info')}
; - else if (!isEmailValid) message ={t('donate.valid-email')}
; - else message ={t('donate.valid-card')}
; - return