fix(donate): refactor handlers for charges

This commit is contained in:
Mrugesh Mohapatra
2019-02-07 19:03:18 +05:30
committed by Stuart Taylor
parent e84f021d8b
commit 02e6e711cf
8 changed files with 88 additions and 69 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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