diff --git a/api-server/server/boot/donate.js b/api-server/server/boot/donate.js index 365fc716ab..f63a4ac4de 100644 --- a/api-server/server/boot/donate.js +++ b/api-server/server/boot/donate.js @@ -170,16 +170,6 @@ export default function donateBoot(app, done) { }; return Promise.resolve(user) - .then(nonDonatingUser => { - const { isDonating } = nonDonatingUser; - if (isDonating) { - throw { - message: `User already has active donation(s).`, - type: 'AlreadyDonatingError' - }; - } - return nonDonatingUser; - }) .then(createCustomer) .then(customer => { return duration === 'onetime' @@ -194,10 +184,7 @@ export default function donateBoot(app, done) { }) .then(createAsyncUserDonation) .catch(err => { - if ( - err.type === 'StripeCardError' || - err.type === 'AlreadyDonatingError' - ) { + if (err.type === 'StripeCardError') { return res.status(402).send({ error: err.message }); } return res diff --git a/client/src/client-only-routes/ShowCertification.js b/client/src/client-only-routes/ShowCertification.js index 7fd0e6b24a..04b35718bb 100644 --- a/client/src/client-only-routes/ShowCertification.js +++ b/client/src/client-only-routes/ShowCertification.js @@ -7,7 +7,7 @@ import { createSelector } from 'reselect'; import { Grid, Row, Col, Image, Button } from '@freecodecamp/react-bootstrap'; import FreeCodeCampLogo from '../assets/icons/freeCodeCampLogo'; // eslint-disable-next-line max-len -import MinimalDonateForm from '../components/Donation/components/MinimalDonateForm'; +import MinimalDonateForm from '../components/Donation/MinimalDonateForm'; import { showCertSelector, diff --git a/client/src/components/Donation/components/DonateCompletion.js b/client/src/components/Donation/DonateCompletion.js similarity index 98% rename from client/src/components/Donation/components/DonateCompletion.js rename to client/src/components/Donation/DonateCompletion.js index 7cfb3a4434..28d6367a0b 100644 --- a/client/src/components/Donation/components/DonateCompletion.js +++ b/client/src/components/Donation/DonateCompletion.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Alert, Button } from '@freecodecamp/react-bootstrap'; import Spinner from 'react-spinkit'; -import '../Donation.css'; +import './Donation.css'; const propTypes = { error: PropTypes.string, diff --git a/client/src/components/Donation/components/DonateForm.js b/client/src/components/Donation/DonateForm.js similarity index 91% rename from client/src/components/Donation/components/DonateForm.js rename to client/src/components/Donation/DonateForm.js index 573756fd3a..a0cd1fe5b0 100644 --- a/client/src/components/Donation/components/DonateForm.js +++ b/client/src/components/Donation/DonateForm.js @@ -18,19 +18,17 @@ import { durationsConfig, defaultAmount, defaultStateConfig -} from '../../../../../config/donation-settings'; +} from '../../../../config/donation-settings'; import { apiLocation } from '../../../../config/env.json'; -import Spacer from '../../helpers/Spacer'; +import Spacer from '../helpers/Spacer'; import DonateFormChildViewForHOC from './DonateFormChildViewForHOC'; import { - userSelector, isSignedInSelector, signInLoadingSelector, hardGoTo as navigate -} from '../../../redux'; +} from '../../redux'; -import '../Donation.css'; -import DonateCompletion from './DonateCompletion.js'; +import './Donation.css'; const numToCommas = num => num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); @@ -46,11 +44,9 @@ const propTypes = { }; const mapStateToProps = createSelector( - userSelector, signInLoadingSelector, isSignedInSelector, - ({ isDonating }, showLoading, isSignedIn) => ({ - isDonating, + (showLoading, isSignedIn) => ({ isSignedIn, showLoading }) @@ -74,8 +70,7 @@ class DonateForm extends Component { this.state = { ...defaultStateConfig, - processing: false, - isDonating: this.props.isDonating + processing: false }; this.getActiveDonationAmount = this.getActiveDonationAmount.bind(this); @@ -222,17 +217,7 @@ class DonateForm extends Component { } render() { - const { isSignedIn, navigate, showLoading, isDonating } = this.props; - - if (isDonating) { - return ( - - - - - - ); - } + const { isSignedIn, navigate, showLoading } = this.props; return ( diff --git a/client/src/components/Donation/components/DonateFormChildViewForHOC.js b/client/src/components/Donation/DonateFormChildViewForHOC.js similarity index 98% rename from client/src/components/Donation/components/DonateFormChildViewForHOC.js rename to client/src/components/Donation/DonateFormChildViewForHOC.js index 7e58e97dae..6a7545644a 100644 --- a/client/src/components/Donation/components/DonateFormChildViewForHOC.js +++ b/client/src/components/Donation/DonateFormChildViewForHOC.js @@ -15,8 +15,8 @@ import { injectStripe } from 'react-stripe-elements'; import StripeCardForm from './StripeCardForm'; import DonateCompletion from './DonateCompletion'; -import { postChargeStripe } from '../../../utils/ajax'; -import { userSelector } from '../../../redux'; +import { postChargeStripe } from '../../utils/ajax'; +import { userSelector } from '../../redux'; const propTypes = { showCloseBtn: PropTypes.func, diff --git a/client/src/components/Donation/DonateText.js b/client/src/components/Donation/DonateText.js new file mode 100644 index 0000000000..6898a2c942 --- /dev/null +++ b/client/src/components/Donation/DonateText.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { Row, Col } from '@freecodecamp/react-bootstrap'; + +const DonateText = () => { + return ( + + +

freeCodeCamp is a highly efficient education nonprofit.

+

+ In 2019 alone, we provided 18 million hours of free education to + people around the world. +

+

+ Since freeCodeCamp's total budget is only $373,000, that means every + dollar you donate to freeCodeCamp translates into 50 hours worth of + technology education. +

+

+ When you donate to freeCodeCamp, you help people learn new skills and + provide for their families. +

+

+ You also help us create new resources for you to use to expand your + own technology skills. +

+
+

+ Need help with your existing or past donations? +

+

+ Send an email to team@freeCodeCamp.org with a copy of your donation + receipt and we will be happy to resolve your query. +

+ +
+ ); +}; +DonateText.displayName = 'DonateText'; +export default DonateText; diff --git a/client/src/components/Donation/components/DonationModal.js b/client/src/components/Donation/DonationModal.js similarity index 89% rename from client/src/components/Donation/components/DonationModal.js rename to client/src/components/Donation/DonationModal.js index be69eb997b..ae12ac7981 100644 --- a/client/src/components/Donation/components/DonationModal.js +++ b/client/src/components/Donation/DonationModal.js @@ -5,22 +5,22 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap'; -import { Spacer } from '../../../components/helpers'; -import { blockNameify } from '../../../../utils/blockNameify'; -import Heart from '../../../assets/icons/Heart'; -import Cup from '../../../assets/icons/Cup'; +import { Spacer } from '../helpers'; +import { blockNameify } from '../../../utils/blockNameify'; +import Heart from '../../assets/icons/Heart'; +import Cup from '../../assets/icons/Cup'; import MinimalDonateForm from './MinimalDonateForm'; -import ga from '../../../analytics'; +import ga from '../../analytics'; import { closeDonationModal, isDonationModalOpenSelector, isBlockDonationModalSelector -} from '../../../redux'; +} from '../../redux'; -import { challengeMetaSelector } from '../../../templates/Challenges/redux'; +import { challengeMetaSelector } from '../../templates/Challenges/redux'; -import '../Donation.css'; +import './Donation.css'; const mapStateToProps = createSelector( isDonationModalOpenSelector, diff --git a/client/src/components/Donation/components/MinimalDonateForm.js b/client/src/components/Donation/MinimalDonateForm.js similarity index 50% rename from client/src/components/Donation/components/MinimalDonateForm.js rename to client/src/components/Donation/MinimalDonateForm.js index 56d9368de2..1b32441f73 100644 --- a/client/src/components/Donation/components/MinimalDonateForm.js +++ b/client/src/components/Donation/MinimalDonateForm.js @@ -1,5 +1,3 @@ -/* eslint-disable react/sort-prop-types */ -/* eslint-disable react/jsx-sort-props */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; @@ -10,23 +8,19 @@ import { StripeProvider, Elements } from 'react-stripe-elements'; import { amountsConfig, durationsConfig, - defaultStateConfig -} from '../../../../../config/donation-settings'; + modalDefaultStateConfig +} from '../../../../config/donation-settings'; +import { stripePublicKey } from '../../../../config/env.json'; +import { stripeScriptLoader } from '../../utils/scriptLoaders'; import DonateFormChildViewForHOC from './DonateFormChildViewForHOC'; -import { userSelector } from '../../../redux'; +import { userSelector } from '../../redux'; -import '../Donation.css'; -import DonateCompletion from './DonateCompletion.js'; -import { stripePublicKey } from '../../../../../config/env.json'; -import { stripeScriptLoader } from '../../../utils/scriptLoaders'; - -const numToCommas = num => - num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); +import './Donation.css'; const propTypes = { - showCloseBtn: PropTypes.func, defaultTheme: PropTypes.string, isDonating: PropTypes.bool, + showCloseBtn: PropTypes.func, stripe: PropTypes.shape({ createToken: PropTypes.func.isRequired }) @@ -39,7 +33,7 @@ const mapStateToProps = createSelector( }) ); -class ModalDonateForm extends Component { +class MinimalDonateForm extends Component { constructor(...args) { super(...args); @@ -47,13 +41,11 @@ class ModalDonateForm extends Component { this.amounts = amountsConfig; this.state = { - ...defaultStateConfig, + ...modalDefaultStateConfig, isDonating: this.props.isDonating, stripe: null }; - this.handleSelectPaymentType = this.handleSelectPaymentType.bind(this); this.handleStripeLoad = this.handleStripeLoad.bind(this); - this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this); } componentDidMount() { @@ -85,91 +77,36 @@ class ModalDonateForm extends Component { } } - handleSelectPaymentType(e) { - this.setState({ - paymentType: e.currentTarget.value - }); - } - - getFormatedAmountLabel(amount) { - return `$${numToCommas(amount / 100)}`; - } - - getDonationButtonLabel() { - const { donationAmount, donationDuration } = this.state; - let donationBtnLabel = `Confirm your donation`; - if (donationDuration === 'onetime') { - donationBtnLabel = `Confirm your one-time donation of ${this.getFormatedAmountLabel( - donationAmount - )}`; - } else { - donationBtnLabel = `Confirm your donation of ${this.getFormatedAmountLabel( - donationAmount - )} ${donationDuration === 'month' ? 'per month' : 'per year'}`; - } - return donationBtnLabel; - } - - renderDonationOptions() { - const { - donationAmount, - donationDuration, - paymentType, - stripe - } = this.state; - - const { showCloseBtn, defaultTheme } = this.props; - return ( -
- {paymentType === 'Card' ? ( - - - - - - ) : ( -

- PayPal is currently unavailable. Please use a Credit/Debit card - instead. -

- )} -
- ); - } - render() { - const { isDonating } = this.props; - - if (isDonating) { - return ( - - - - - - ); - } + const { donationAmount, donationDuration, stripe } = this.state; + const { showCloseBtn, defaultTheme } = this.props; return ( - {this.renderDonationOptions()} + + + + `Confirm your donation of $5 per month` + } + showCloseBtn={showCloseBtn} + /> + + ); } } -ModalDonateForm.displayName = 'ModalDonateForm'; -ModalDonateForm.propTypes = propTypes; +MinimalDonateForm.displayName = 'MinimalDonateForm'; +MinimalDonateForm.propTypes = propTypes; export default connect( mapStateToProps, null -)(ModalDonateForm); +)(MinimalDonateForm); diff --git a/client/src/components/Donation/components/StripeCardForm.js b/client/src/components/Donation/StripeCardForm.js similarity index 100% rename from client/src/components/Donation/components/StripeCardForm.js rename to client/src/components/Donation/StripeCardForm.js diff --git a/client/src/components/Donation/components/DonateText.js b/client/src/components/Donation/components/DonateText.js deleted file mode 100644 index a42da163b6..0000000000 --- a/client/src/components/Donation/components/DonateText.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; - -import { Spacer, Link } from '../../../components/helpers'; - -const DonateText = () => { - return ( -
- -

How to donate to freeCodeCamp.org

- -

freeCodeCamp is a tax-exempt 501(c)(3) public charity.

- -

- We get almost all of our budget from our supporters, who donate $5 per - month to our nonprofit. -

- -

- To become a supporter, just{' '} - start going through the curriculum and you will - see a prompt to donate. -

- -

- If you want to make a larger one-time donation, set up employer - matching, or support us in other ways, email team@freecodecamp.org and - we can help make that happen. -

- - -

How does freeCodeCamp use these donations?

- -

- 100% of donations go to pay for servers, and to pay teachers and - developers who help build our learning resources. -

- -

- We earned the 2019 Platinum Seal of Transparency from Guidestar.org. You - can view all our nonprofit's details and download our accounting - documents{' '} - there. -

- - -

How do I stop my monthly recurring donation.

- -

- Easy. Just forward a donation receipt to team@freecodecamp.org and we'll - stop it. -

- - -

How do I restart my monthly recurring donation?

- -

- Email one of your old donation receipts to team@freecodecamp.org and - we'll restart it for you. -

-
- ); -}; - -DonateText.displayName = 'DonateText'; - -export default DonateText; diff --git a/client/src/components/YearEndGift/YearEndDonationForm.js b/client/src/components/YearEndGift/YearEndDonationForm.js index 09da340637..0bbafc32fd 100644 --- a/client/src/components/YearEndGift/YearEndDonationForm.js +++ b/client/src/components/YearEndGift/YearEndDonationForm.js @@ -14,7 +14,7 @@ import { StripeProvider, Elements } from 'react-stripe-elements'; import { Spacer } from '../helpers'; // eslint-disable-next-line max-len -import DonateFormChildViewForHOC from '../Donation/components/DonateFormChildViewForHOC'; +import DonateFormChildViewForHOC from '../Donation/DonateFormChildViewForHOC'; import './YearEndGift.css'; import '../Donation/Donation.css'; diff --git a/client/src/components/layouts/Learn.js b/client/src/components/layouts/Learn.js index 945d3172e0..a24bdb9ce1 100644 --- a/client/src/components/layouts/Learn.js +++ b/client/src/components/layouts/Learn.js @@ -11,7 +11,7 @@ import { tryToShowDonationModal } from '../../redux'; import createRedirect from '../../components/createRedirect'; -import DonateModal from '../Donation/components/DonationModal'; +import DonateModal from '../Donation/DonationModal'; import 'prismjs/themes/prism.css'; import './prism.css'; diff --git a/client/src/pages/donate.js b/client/src/pages/donate.js index 8d9bfac705..ce1467069a 100644 --- a/client/src/pages/donate.js +++ b/client/src/pages/donate.js @@ -1,29 +1,108 @@ -import React, { Fragment } from 'react'; +import React, { Component, Fragment } from 'react'; import Helmet from 'react-helmet'; -import { Grid } from '@freecodecamp/react-bootstrap'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { Grid, Row, Col } from '@freecodecamp/react-bootstrap'; -import { Spacer, FullWidthRow } from '../components/helpers'; -import DonateText from '../components/Donation/components/DonateText'; +import { stripePublicKey } from '../../config/env.json'; +import { Spacer, Loader } from '../components/helpers'; +import DonateForm from '../components/Donation/DonateForm'; +import DonateText from '../components/Donation/DonateText'; +import { signInLoadingSelector, userSelector } from '../redux'; +import { stripeScriptLoader } from '../utils/scriptLoaders'; -function DonatePage() { - return ( - - - -
+const propTypes = { + isDonating: PropTypes.bool, + showLoading: PropTypes.bool.isRequired +}; + +const mapStateToProps = createSelector( + userSelector, + signInLoadingSelector, + ({ isDonating }, showLoading) => ({ + isDonating, + showLoading + }) +); + +export class DonatePage extends Component { + constructor(...props) { + super(...props); + this.state = { + stripe: null, + enableSettings: false + }; + + this.handleStripeLoad = this.handleStripeLoad.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); + } + } + + handleStripeLoad() { + // Create Stripe instance once Stripe.js loads + console.info('stripe has loaded'); + this.setState(state => ({ + ...state, + stripe: window.Stripe(stripePublicKey) + })); + } + + render() { + const { stripe } = this.state; + const { showLoading } = this.props; + + if (showLoading) { + return ; + } + + return ( + + + - -

Become a Supporter

- -
+ + +

Become a Supporter

+ + +
+ + + + + + + + - -
-
-
- ); + + + ); + } } DonatePage.displayName = 'DonatePage'; +DonatePage.propTypes = propTypes; -export default DonatePage; +export default connect(mapStateToProps)(DonatePage); diff --git a/config/donation-settings.js b/config/donation-settings.js index b68a0a9086..89b2cbc755 100644 --- a/config/donation-settings.js +++ b/config/donation-settings.js @@ -11,7 +11,7 @@ const amountsConfig = { }; const defaultAmount = { year: 25000, - month: 500, + month: 3500, onetime: 25000 }; const defaultStateConfig = { @@ -19,6 +19,11 @@ const defaultStateConfig = { donationDuration: 'month', paymentType: 'Card' }; +const modalDefaultStateConfig = { + donationAmount: 500, + donationDuration: 'month', + paymentType: 'Card' +}; // Configuration for server side const durationKeysConfig = ['year', 'month', 'onetime']; @@ -41,5 +46,6 @@ module.exports = { defaultStateConfig, durationKeysConfig, donationOneTimeConfig, - donationSubscriptionConfig + donationSubscriptionConfig, + modalDefaultStateConfig };