fix(donate): refactor handlers for charges
This commit is contained in:
committed by
Stuart Taylor
parent
e84f021d8b
commit
02e6e711cf
@ -1,6 +1,10 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
import keys from '../../../config/secrets';
|
import keys from '../../../config/secrets';
|
||||||
|
|
||||||
|
const log = debug('fcc:boot:donate');
|
||||||
|
|
||||||
export default function donateBoot(app, done) {
|
export default function donateBoot(app, done) {
|
||||||
|
|
||||||
let stripe = false;
|
let stripe = false;
|
||||||
@ -70,7 +74,19 @@ export default function donateBoot(app, done) {
|
|||||||
|
|
||||||
const fccUser = user ?
|
const fccUser = user ?
|
||||||
Promise.resolve(user) :
|
Promise.resolve(user) :
|
||||||
User.create$({ email }).toPromise();
|
new Promise((resolve, reject) =>
|
||||||
|
User.findOrCreate(
|
||||||
|
{ where: { email }},
|
||||||
|
{ email },
|
||||||
|
(err, instance, isNew) => {
|
||||||
|
log('is new user instance: ', isNew);
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve(instance);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
let donatingUser = {};
|
let donatingUser = {};
|
||||||
let donation = {
|
let donation = {
|
||||||
@ -133,7 +149,8 @@ export default function donateBoot(app, done) {
|
|||||||
api.post('/charge-stripe', createStripeDonation);
|
api.post('/charge-stripe', createStripeDonation);
|
||||||
donateRouter.use('/donate', api);
|
donateRouter.use('/donate', api);
|
||||||
app.use(donateRouter);
|
app.use(donateRouter);
|
||||||
app.use('/external', donateRouter);
|
app.use('/internal', donateRouter);
|
||||||
|
app.use('/unauthenticated', donateRouter);
|
||||||
connectToStripe().then(done);
|
connectToStripe().then(done);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export default function() {
|
|||||||
return function csrf(req, res, next) {
|
return function csrf(req, res, next) {
|
||||||
|
|
||||||
const path = req.path.split('/')[1];
|
const path = req.path.split('/')[1];
|
||||||
if (/(^api$|^external$|^internal$|^p$)/.test(path)) {
|
if (/(^api$|^unauthenticated$|^internal$|^p$)/.test(path)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
return protection(req, res, next);
|
return protection(req, res, next);
|
||||||
|
@ -3,15 +3,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.donation-modal p {
|
.donation-modal p {
|
||||||
width: 90%;
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.donation-modal .alert {
|
||||||
|
width: 60%;
|
||||||
|
margin: 0 auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
.donation-modal .modal-title {
|
.donation-modal .modal-title {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.donation-modal .donation-form {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
.donation-form {
|
.donation-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -26,19 +33,19 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
#donation-completion-body {
|
.donation-completion,
|
||||||
|
.donation-completion-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-completion-buttons {
|
.donation-completion-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
}
|
align-items: center;
|
||||||
|
|
||||||
.donation-completion-buttons button {
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-email-container label {
|
.donation-email-container label {
|
||||||
@ -51,16 +58,16 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maybe-later-container {
|
.modal-close-btn-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maybe-later-container a {
|
.modal-close-btn-container a {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maybe-later-container a:hover {
|
.modal-close-btn-container a:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Alert, Button } from '@freecodecamp/react-bootstrap';
|
import { Alert, Button } from '@freecodecamp/react-bootstrap';
|
||||||
import Spinner from 'react-spinkit';
|
import Spinner from 'react-spinkit';
|
||||||
|
|
||||||
|
import '../Donation.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
processing: PropTypes.bool,
|
processing: PropTypes.bool,
|
||||||
@ -16,13 +18,12 @@ function DonateCompletion({ processing, reset, success, error = null }) {
|
|||||||
const heading = processing
|
const heading = processing
|
||||||
? 'We are processing your donation.'
|
? 'We are processing your donation.'
|
||||||
: success
|
: success
|
||||||
? 'Your donation was successful. Thank you for supporting the ' +
|
? 'Your donation was successful.'
|
||||||
'freeCodeCamp.org community.'
|
|
||||||
: 'Something went wrong with your donation';
|
: 'Something went wrong with your donation';
|
||||||
return (
|
return (
|
||||||
<Alert bsStyle={style}>
|
<Alert bsStyle={style} className='donation-completion'>
|
||||||
<h4>{heading}</h4>
|
<h4>{heading}</h4>
|
||||||
<div id='donation-completion-body'>
|
<div className='donation-completion-body'>
|
||||||
{processing && (
|
{processing && (
|
||||||
<Spinner
|
<Spinner
|
||||||
className='user-state-spinner'
|
className='user-state-spinner'
|
||||||
@ -31,16 +32,24 @@ function DonateCompletion({ processing, reset, success, error = null }) {
|
|||||||
name='line-scale'
|
name='line-scale'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{error && error}
|
{success && (
|
||||||
|
<p>
|
||||||
|
Thank you for supporting the freeCodeCamp.org community.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<p>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className='donation-completion-buttons'>
|
<p className='donation-completion-buttons'>
|
||||||
{error && (
|
{error && (
|
||||||
<Fragment>
|
<div>
|
||||||
<Button bsStyle='primary' onClick={reset}>
|
<Button bsStyle='primary' onClick={reset}>
|
||||||
Try again
|
Try again
|
||||||
</Button>
|
</Button>
|
||||||
<span />
|
</div>
|
||||||
</Fragment>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
import isEmail from 'validator/lib/isEmail';
|
import isEmail from 'validator/lib/isEmail';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -10,14 +12,15 @@ import {
|
|||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import { injectStripe } from 'react-stripe-elements';
|
import { injectStripe } from 'react-stripe-elements';
|
||||||
|
|
||||||
|
import { apiLocation } from '../../../../config/env.json';
|
||||||
import Spacer from '../../../components/helpers/Spacer';
|
import Spacer from '../../../components/helpers/Spacer';
|
||||||
import StripeCardForm from './StripeCardForm';
|
import StripeCardForm from './StripeCardForm';
|
||||||
import DonateCompletion from './DonateCompletion';
|
import DonateCompletion from './DonateCompletion';
|
||||||
import { postJSON$ } from '../../../templates/Challenges/utils/ajax-stream.js';
|
import { postJSON$ } from '../../../templates/Challenges/utils/ajax-stream.js';
|
||||||
|
import { userSelector, isSignedInSelector } from '../../../redux';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
maybeButton: PropTypes.func.isRequired,
|
|
||||||
stripe: PropTypes.shape({
|
stripe: PropTypes.shape({
|
||||||
createToken: PropTypes.func.isRequired
|
createToken: PropTypes.func.isRequired
|
||||||
})
|
})
|
||||||
@ -31,6 +34,12 @@ const initialSate = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
isSignedInSelector,
|
||||||
|
({ email }, isSignedIn) => ({ email, isSignedIn })
|
||||||
|
);
|
||||||
|
|
||||||
class DonateForm extends Component {
|
class DonateForm extends Component {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
@ -50,7 +59,6 @@ class DonateForm extends Component {
|
|||||||
this.submit = this.submit.bind(this);
|
this.submit = this.submit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getUserEmail() {
|
getUserEmail() {
|
||||||
const { email: stateEmail } = this.state;
|
const { email: stateEmail } = this.state;
|
||||||
const { email: propsEmail } = this.props;
|
const { email: propsEmail } = this.props;
|
||||||
@ -103,6 +111,7 @@ class DonateForm extends Component {
|
|||||||
|
|
||||||
postDonation(token) {
|
postDonation(token) {
|
||||||
const { donationAmount: amount } = this.state;
|
const { donationAmount: amount } = this.state;
|
||||||
|
const { isSignedIn } = this.props;
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
donationState: {
|
donationState: {
|
||||||
@ -110,7 +119,10 @@ class DonateForm extends Component {
|
|||||||
processing: true
|
processing: true
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
const chargeStripePath = '/unauthenticated/donate/charge-stripe';
|
|
||||||
|
const chargeStripePath = isSignedIn ?
|
||||||
|
'/internal/donate/charge-stripe' :
|
||||||
|
`${apiLocation}/unauthenticated/donate/charge-stripe`;
|
||||||
return postJSON$(chargeStripePath, {
|
return postJSON$(chargeStripePath, {
|
||||||
token,
|
token,
|
||||||
amount
|
amount
|
||||||
@ -148,9 +160,7 @@ class DonateForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCompletion(props) {
|
renderCompletion(props) {
|
||||||
return (
|
return <DonateCompletion {...props} />;
|
||||||
<DonateCompletion {...props}/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDonateForm() {
|
renderDonateForm() {
|
||||||
@ -163,11 +173,11 @@ class DonateForm extends Component {
|
|||||||
Email (we'll send you a tax-deductible donation receipt):
|
Email (we'll send you a tax-deductible donation receipt):
|
||||||
</ControlLabel>
|
</ControlLabel>
|
||||||
<FormControl
|
<FormControl
|
||||||
onChange={this.handleEmailChange}
|
onChange={this.handleEmailChange}
|
||||||
placeholder='me@example.com'
|
placeholder='me@example.com'
|
||||||
required={true}
|
required={true}
|
||||||
type='text'
|
type='text'
|
||||||
value={this.getUserEmail()}
|
value={this.getUserEmail()}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<StripeCardForm getValidationState={this.getValidationState} />
|
<StripeCardForm getValidationState={this.getValidationState} />
|
||||||
@ -183,7 +193,6 @@ class DonateForm extends Component {
|
|||||||
</Button>
|
</Button>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</Form>
|
</Form>
|
||||||
{this.props.maybeButton()}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -207,4 +216,4 @@ class DonateForm extends Component {
|
|||||||
DonateForm.displayName = 'DonateForm';
|
DonateForm.displayName = 'DonateForm';
|
||||||
DonateForm.propTypes = propTypes;
|
DonateForm.propTypes = propTypes;
|
||||||
|
|
||||||
export default injectStripe(DonateForm);
|
export default injectStripe(connect(mapStateToProps)(DonateForm));
|
||||||
|
@ -11,7 +11,6 @@ import { stripePublicKey } from '../../../../config/env.json';
|
|||||||
import ga from '../../../analytics';
|
import ga from '../../../analytics';
|
||||||
import DonateForm from './DonateForm';
|
import DonateForm from './DonateForm';
|
||||||
import {
|
import {
|
||||||
userSelector,
|
|
||||||
closeDonationModal,
|
closeDonationModal,
|
||||||
isDonationModalOpenSelector
|
isDonationModalOpenSelector
|
||||||
} from '../../../redux';
|
} from '../../../redux';
|
||||||
@ -22,9 +21,8 @@ import DonateText from './DonateText';
|
|||||||
import '../Donation.css';
|
import '../Donation.css';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userSelector,
|
|
||||||
isDonationModalOpenSelector,
|
isDonationModalOpenSelector,
|
||||||
({ email }, show) => ({ email, show })
|
show => ({ show })
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
@ -37,7 +35,6 @@ const mapDispatchToProps = dispatch =>
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
closeDonationModal: PropTypes.func.isRequired,
|
closeDonationModal: PropTypes.func.isRequired,
|
||||||
email: PropTypes.string,
|
|
||||||
show: PropTypes.bool
|
show: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,14 +73,14 @@ class DonateModal extends Component {
|
|||||||
return closeDonationModal();
|
return closeDonationModal();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className='maybe-later-container'>
|
<div className='modal-close-btn-container'>
|
||||||
<Button bsStyle='link' onClick={handleClick}>Maybe later</Button>
|
<Button bsStyle='link' onClick={handleClick}>Close</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { email, show } = this.props;
|
const { show } = this.props;
|
||||||
if (show) {
|
if (show) {
|
||||||
ga.modalview('/donation-modal');
|
ga.modalview('/donation-modal');
|
||||||
}
|
}
|
||||||
@ -97,11 +94,9 @@ class DonateModal extends Component {
|
|||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<DonateText/>
|
<DonateText />
|
||||||
<DonateForm
|
<DonateForm />
|
||||||
email={email}
|
{this.renderMaybe()}
|
||||||
maybeButton={this.renderMaybe}
|
|
||||||
/>
|
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<PoweredByStripe />
|
<PoweredByStripe />
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { StripeProvider, Elements } from 'react-stripe-elements';
|
import { StripeProvider, Elements } from 'react-stripe-elements';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { Row, Col } from '@freecodecamp/react-bootstrap';
|
import { Row, Col } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
import { stripePublicKey } from '../../config/env.json';
|
import { stripePublicKey } from '../../config/env.json';
|
||||||
import { userSelector } from '../redux';
|
|
||||||
|
|
||||||
import Spacer from '../components/helpers/Spacer';
|
import Spacer from '../components/helpers/Spacer';
|
||||||
import DonateForm from '../components/Donation/components/DonateForm';
|
import DonateForm from '../components/Donation/components/DonateForm';
|
||||||
@ -17,15 +13,6 @@ import PoweredByStripe from '../components/Donation/components/poweredByStripe';
|
|||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
email: PropTypes.string,
|
|
||||||
show: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = createSelector(userSelector, ({ email = '' }) => ({
|
|
||||||
email
|
|
||||||
}));
|
|
||||||
|
|
||||||
class IndexPage extends Component {
|
class IndexPage extends Component {
|
||||||
constructor(...props) {
|
constructor(...props) {
|
||||||
super(...props);
|
super(...props);
|
||||||
@ -66,7 +53,6 @@ class IndexPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { email = '' } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Helmet title='Support the freeCodeCamp.org nonprofit' />
|
<Helmet title='Support the freeCodeCamp.org nonprofit' />
|
||||||
@ -76,16 +62,13 @@ class IndexPage extends Component {
|
|||||||
<h2 className='text-center'>
|
<h2 className='text-center'>
|
||||||
Become a Supporter
|
Become a Supporter
|
||||||
</h2>
|
</h2>
|
||||||
<DonateText/>
|
<DonateText />
|
||||||
</Col>
|
</Col>
|
||||||
<Col sm={6} smOffset={3} xs={12}>
|
<Col sm={6} smOffset={3} xs={12}>
|
||||||
<hr />
|
<hr />
|
||||||
<StripeProvider stripe={this.state.stripe}>
|
<StripeProvider stripe={this.state.stripe}>
|
||||||
<Elements>
|
<Elements>
|
||||||
<DonateForm
|
<DonateForm />
|
||||||
email={email}
|
|
||||||
maybeButton={() => null}
|
|
||||||
/>
|
|
||||||
</Elements>
|
</Elements>
|
||||||
</StripeProvider>
|
</StripeProvider>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
@ -102,6 +85,5 @@ class IndexPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IndexPage.displayName = 'IndexPage';
|
IndexPage.displayName = 'IndexPage';
|
||||||
IndexPage.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(IndexPage);
|
export default IndexPage;
|
||||||
|
@ -6,5 +6,5 @@ export function createGuideUrl(slug = '') {
|
|||||||
|
|
||||||
export function isGoodXHRStatus(status) {
|
export function isGoodXHRStatus(status) {
|
||||||
const statusInt = parseInt(status, 10);
|
const statusInt = parseInt(status, 10);
|
||||||
return statusInt >= 200 && statusInt < 400;
|
return (statusInt >= 200 && statusInt < 400) || statusInt === 402;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user