fix(donate): implement donate page

This commit is contained in:
Mrugesh Mohapatra
2019-12-20 16:14:16 +05:30
committed by mrugesh
parent 5609a1303e
commit e4590fed5c
14 changed files with 195 additions and 228 deletions

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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 (
<Row>
<Col sm={10} smOffset={1} xs={12}>
<DonateCompletion success={true} />
</Col>
</Row>
);
}
const { isSignedIn, navigate, showLoading } = this.props;
return (
<Row>

View File

@ -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,

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Row, Col } from '@freecodecamp/react-bootstrap';
const DonateText = () => {
return (
<Row className='donate-text'>
<Col sm={10} smOffset={1} xs={12}>
<p>freeCodeCamp is a highly efficient education nonprofit.</p>
<p>
In 2019 alone, we provided 18 million hours of free education to
people around the world.
</p>
<p>
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.
</p>
<p>
When you donate to freeCodeCamp, you help people learn new skills and
provide for their families.
</p>
<p>
You also help us create new resources for you to use to expand your
own technology skills.
</p>
<hr />
<h4>
<b>Need help with your existing or past donations?</b>
</h4>
<p>
Send an email to team@freeCodeCamp.org with a copy of your donation
receipt and we will be happy to resolve your query.
</p>
</Col>
</Row>
);
};
DonateText.displayName = 'DonateText';
export default DonateText;

View File

@ -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,

View File

@ -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 (
<div>
{paymentType === 'Card' ? (
<StripeProvider stripe={stripe}>
<Elements>
<DonateFormChildViewForHOC
showCloseBtn={showCloseBtn}
defaultTheme={defaultTheme}
donationAmount={donationAmount}
donationDuration={donationDuration}
getDonationButtonLabel={this.getDonationButtonLabel}
/>
</Elements>
</StripeProvider>
) : (
<p>
PayPal is currently unavailable. Please use a Credit/Debit card
instead.
</p>
)}
</div>
);
}
render() {
const { isDonating } = this.props;
if (isDonating) {
return (
<Row>
<Col sm={10} smOffset={1} xs={12}>
<DonateCompletion success={true} />
</Col>
</Row>
);
}
const { donationAmount, donationDuration, stripe } = this.state;
const { showCloseBtn, defaultTheme } = this.props;
return (
<Row>
<Col sm={10} smOffset={1} xs={12}>
{this.renderDonationOptions()}
<StripeProvider stripe={stripe}>
<Elements>
<DonateFormChildViewForHOC
defaultTheme={defaultTheme}
donationAmount={donationAmount}
donationDuration={donationDuration}
getDonationButtonLabel={() =>
`Confirm your donation of $5 per month`
}
showCloseBtn={showCloseBtn}
/>
</Elements>
</StripeProvider>
</Col>
</Row>
);
}
}
ModalDonateForm.displayName = 'ModalDonateForm';
ModalDonateForm.propTypes = propTypes;
MinimalDonateForm.displayName = 'MinimalDonateForm';
MinimalDonateForm.propTypes = propTypes;
export default connect(
mapStateToProps,
null
)(ModalDonateForm);
)(MinimalDonateForm);

View File

@ -1,66 +0,0 @@
import React from 'react';
import { Spacer, Link } from '../../../components/helpers';
const DonateText = () => {
return (
<div className='donate-text'>
<Spacer />
<h3>How to donate to freeCodeCamp.org</h3>
<p>freeCodeCamp is a tax-exempt 501(c)(3) public charity.</p>
<p>
We get almost all of our budget from our supporters, who donate $5 per
month to our nonprofit.
</p>
<p>
To become a supporter, just{' '}
<Link to='/learn'>start going through the curriculum</Link> and you will
see a prompt to donate.
</p>
<p>
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.
</p>
<Spacer />
<h3> How does freeCodeCamp use these donations?</h3>
<p>
100% of donations go to pay for servers, and to pay teachers and
developers who help build our learning resources.
</p>
<p>
We earned the 2019 Platinum Seal of Transparency from Guidestar.org. You
can view all our nonprofit's details and download our accounting
documents{' '}
<Link to='https://www.guidestar.org/profile/82-0779546'>there.</Link>
</p>
<Spacer />
<h3> How do I stop my monthly recurring donation.</h3>
<p>
Easy. Just forward a donation receipt to team@freecodecamp.org and we'll
stop it.
</p>
<Spacer />
<h3>How do I restart my monthly recurring donation?</h3>
<p>
Email one of your old donation receipts to team@freecodecamp.org and
we'll restart it for you.
</p>
</div>
);
};
DonateText.displayName = 'DonateText';
export default DonateText;

View File

@ -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';

View File

@ -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';

View File

@ -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 (
<Fragment>
<Helmet title='Support our nonprofit | freeCodeCamp.org' />
<Grid>
<main>
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 <Loader fullScreen={true} />;
}
return (
<Fragment>
<Helmet title='Support our nonprofit | freeCodeCamp.org' />
<Grid>
<Spacer />
<FullWidthRow>
<h1 className='text-center'>Become a Supporter</h1>
<DonateText />
</FullWidthRow>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<h1 className='text-center'>Become a Supporter</h1>
<Spacer />
</Col>
</Row>
<Row>
<Col md={6}>
<DonateForm
enableDonationSettingsPage={this.enableDonationSettingsPage}
stripe={stripe}
/>
</Col>
<Col md={6}>
<DonateText />
</Col>
</Row>
<Spacer />
<Spacer />
</main>
</Grid>
</Fragment>
);
</Grid>
</Fragment>
);
}
}
DonatePage.displayName = 'DonatePage';
DonatePage.propTypes = propTypes;
export default DonatePage;
export default connect(mapStateToProps)(DonatePage);

View File

@ -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
};