fix(client): unify client donations methods (#39562)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -8,7 +8,7 @@ import format from 'date-fns/format';
|
||||
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/MinimalDonateForm';
|
||||
import DonateForm from '../components/Donation/DonateForm';
|
||||
|
||||
import {
|
||||
showCertSelector,
|
||||
@ -233,9 +233,10 @@ class ShowCertification extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<MinimalDonateForm
|
||||
<DonateForm
|
||||
handleProcessing={this.handleProcessing}
|
||||
defaultTheme='light'
|
||||
isMinimalForm={true}
|
||||
/>
|
||||
<Row>
|
||||
<Col sm={4} smOffset={4} xs={6} xsOffset={3}>
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { StripeProvider, Elements } from 'react-stripe-elements';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
@ -18,15 +19,28 @@ import {
|
||||
amountsConfig,
|
||||
durationsConfig,
|
||||
defaultAmount,
|
||||
defaultStateConfig,
|
||||
defaultDonation,
|
||||
onetimeSKUConfig,
|
||||
donationUrls
|
||||
donationUrls,
|
||||
modalDefaultDonation
|
||||
} from '../../../../config/donation-settings';
|
||||
import { stripePublicKey } from '../../../../config/env.json';
|
||||
import { stripeScriptLoader } from '../../utils/scriptLoaders';
|
||||
import DonateFormChildViewForHOC from './DonateFormChildViewForHOC';
|
||||
import { deploymentEnv } from '../../../config/env.json';
|
||||
import Spacer from '../helpers/Spacer';
|
||||
import PaypalButton from './PaypalButton';
|
||||
import DonateCompletion from './DonateCompletion';
|
||||
import { isSignedInSelector, signInLoadingSelector } from '../../redux';
|
||||
import {
|
||||
isSignedInSelector,
|
||||
signInLoadingSelector,
|
||||
donationFormStateSelector,
|
||||
hardGoTo as navigate,
|
||||
addDonation,
|
||||
postChargeStripe,
|
||||
updateDonationFormState,
|
||||
defaultDonationFormState
|
||||
} from '../../redux';
|
||||
|
||||
import './Donation.css';
|
||||
|
||||
@ -34,32 +48,35 @@ const numToCommas = num =>
|
||||
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
|
||||
const propTypes = {
|
||||
addDonation: PropTypes.func,
|
||||
defaultTheme: PropTypes.string,
|
||||
donationFormState: PropTypes.object,
|
||||
handleProcessing: PropTypes.func,
|
||||
isDonating: PropTypes.bool,
|
||||
isMinimalForm: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
postChargeStripe: PropTypes.func.isRequired,
|
||||
showLoading: PropTypes.bool.isRequired,
|
||||
stripe: PropTypes.shape({
|
||||
createToken: PropTypes.func.isRequired,
|
||||
redirectToCheckout: PropTypes.func.isRequired
|
||||
})
|
||||
updateDonationFormState: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
signInLoadingSelector,
|
||||
isSignedInSelector,
|
||||
(showLoading, isSignedIn) => ({
|
||||
donationFormStateSelector,
|
||||
(showLoading, isSignedIn, donationFormState) => ({
|
||||
isSignedIn,
|
||||
showLoading
|
||||
showLoading,
|
||||
donationFormState
|
||||
})
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
donationState: {
|
||||
processing: false,
|
||||
success: false,
|
||||
error: ''
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
addDonation,
|
||||
navigate,
|
||||
postChargeStripe,
|
||||
updateDonationFormState
|
||||
};
|
||||
|
||||
class DonateForm extends Component {
|
||||
@ -69,12 +86,17 @@ class DonateForm extends Component {
|
||||
this.durations = durationsConfig;
|
||||
this.amounts = amountsConfig;
|
||||
|
||||
const initialAmountAndDuration = this.props.isMinimalForm
|
||||
? modalDefaultDonation
|
||||
: defaultDonation;
|
||||
|
||||
this.state = {
|
||||
...initialState,
|
||||
...defaultStateConfig,
|
||||
processing: false
|
||||
...initialAmountAndDuration,
|
||||
processing: false,
|
||||
stripe: null
|
||||
};
|
||||
|
||||
this.handleStripeLoad = this.handleStripeLoad.bind(this);
|
||||
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
||||
this.getActiveDonationAmount = this.getActiveDonationAmount.bind(this);
|
||||
this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this);
|
||||
@ -87,17 +109,43 @@ class DonateForm extends Component {
|
||||
this.resetDonation = this.resetDonation.bind(this);
|
||||
}
|
||||
|
||||
onDonationStateChange(success, processing, error) {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
donationState: {
|
||||
...state.donationState,
|
||||
processing: processing,
|
||||
success: success,
|
||||
error: error
|
||||
}
|
||||
}));
|
||||
if (success) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -126,7 +174,7 @@ class DonateForm extends Component {
|
||||
} else {
|
||||
donationBtnLabel = `Confirm your donation of ${this.getFormatedAmountLabel(
|
||||
donationAmount
|
||||
)} ${donationDuration === 'month' ? 'per month' : 'per year'}`;
|
||||
)} ${donationDuration === 'month' ? ' / month' : ' / year'}`;
|
||||
}
|
||||
return donationBtnLabel;
|
||||
}
|
||||
@ -140,10 +188,16 @@ class DonateForm extends Component {
|
||||
this.setState({ donationAmount });
|
||||
}
|
||||
|
||||
async handleStripeCheckoutRedirect(e) {
|
||||
const { stripe } = this.props;
|
||||
async handleStripeCheckoutRedirect(e, paymentMethod) {
|
||||
const { stripe } = this.state;
|
||||
const { donationAmount, donationDuration } = this.state;
|
||||
|
||||
this.props.handleProcessing(
|
||||
donationDuration,
|
||||
donationAmount,
|
||||
`stripe (${paymentMethod}) button click`
|
||||
);
|
||||
|
||||
const isOneTime = donationDuration === 'onetime';
|
||||
const getSKUId = () => {
|
||||
const { id } = onetimeSKUConfig[deploymentEnv || 'staging'].find(
|
||||
@ -236,7 +290,7 @@ class DonateForm extends Component {
|
||||
}
|
||||
|
||||
renderDonationOptions() {
|
||||
const { handleProcessing, isSignedIn } = this.props;
|
||||
const { handleProcessing, isSignedIn, addDonation } = this.props;
|
||||
const { donationAmount, donationDuration } = this.state;
|
||||
|
||||
const isOneTime = donationDuration === 'onetime';
|
||||
@ -257,7 +311,7 @@ class DonateForm extends Component {
|
||||
bsStyle='primary'
|
||||
className='btn-cta'
|
||||
id='confirm-donation-btn'
|
||||
onClick={this.handleStripeCheckoutRedirect}
|
||||
onClick={e => this.handleStripeCheckoutRedirect(e, 'apple pay')}
|
||||
>
|
||||
<span>Donate with Apple Pay</span>
|
||||
|
||||
@ -269,7 +323,7 @@ class DonateForm extends Component {
|
||||
bsStyle='primary'
|
||||
className='btn-cta'
|
||||
id='confirm-donation-btn'
|
||||
onClick={this.handleStripeCheckoutRedirect}
|
||||
onClick={e => this.handleStripeCheckoutRedirect(e, 'google pay')}
|
||||
>
|
||||
<span>Donate with Google Pay</span>
|
||||
<GooglePay className='google-pay-logo' />
|
||||
@ -280,7 +334,7 @@ class DonateForm extends Component {
|
||||
bsStyle='primary'
|
||||
className='btn-cta'
|
||||
id='confirm-donation-btn'
|
||||
onClick={this.handleStripeCheckoutRedirect}
|
||||
onClick={e => this.handleStripeCheckoutRedirect(e, 'credit card')}
|
||||
>
|
||||
<span>Donate with Card</span>
|
||||
|
||||
@ -292,6 +346,7 @@ class DonateForm extends Component {
|
||||
</Button>
|
||||
<Spacer />
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
handleProcessing={handleProcessing}
|
||||
@ -305,25 +360,59 @@ class DonateForm extends Component {
|
||||
}
|
||||
|
||||
resetDonation() {
|
||||
return this.setState({ ...initialState });
|
||||
return this.props.updateDonationFormState({ ...defaultDonationFormState });
|
||||
}
|
||||
|
||||
renderCompletion(props) {
|
||||
return <DonateCompletion {...props} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
renderModalForm() {
|
||||
const { donationAmount, donationDuration, stripe } = this.state;
|
||||
const {
|
||||
donationState: { processing, success, error }
|
||||
} = this.state;
|
||||
if (processing || success || error) {
|
||||
return this.renderCompletion({
|
||||
processing,
|
||||
success,
|
||||
error,
|
||||
reset: this.resetDonation
|
||||
});
|
||||
}
|
||||
handleProcessing,
|
||||
defaultTheme,
|
||||
addDonation,
|
||||
postChargeStripe
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<b>{this.getDonationButtonLabel()} with PayPal:</b>
|
||||
<Spacer />
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
handleProcessing={handleProcessing}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<b>Or donate with a credit card:</b>
|
||||
<Spacer />
|
||||
<StripeProvider stripe={stripe}>
|
||||
<Elements>
|
||||
<DonateFormChildViewForHOC
|
||||
defaultTheme={defaultTheme}
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
getDonationButtonLabel={this.getDonationButtonLabel}
|
||||
handleProcessing={handleProcessing}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
postChargeStripe={postChargeStripe}
|
||||
/>
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
renderPageForm() {
|
||||
return (
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
@ -335,9 +424,45 @@ class DonateForm extends Component {
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
donationFormState: { processing, success, error },
|
||||
isMinimalForm
|
||||
} = this.props;
|
||||
if (success || error) {
|
||||
return this.renderCompletion({
|
||||
processing,
|
||||
success,
|
||||
error,
|
||||
reset: this.resetDonation
|
||||
});
|
||||
}
|
||||
|
||||
// keep payment provider elements on DOM during processing to avoid errors.
|
||||
return (
|
||||
<>
|
||||
{processing &&
|
||||
this.renderCompletion({
|
||||
processing,
|
||||
success,
|
||||
error,
|
||||
reset: this.resetDonation
|
||||
})}
|
||||
<div className={processing ? 'hide' : ''}>
|
||||
{isMinimalForm
|
||||
? this.renderModalForm(processing)
|
||||
: this.renderPageForm(processing)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DonateForm.displayName = 'DonateForm';
|
||||
DonateForm.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps)(DonateForm);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DonateForm);
|
||||
|
@ -15,7 +15,6 @@ import { injectStripe } from 'react-stripe-elements';
|
||||
|
||||
import StripeCardForm from './StripeCardForm';
|
||||
import DonateCompletion from './DonateCompletion';
|
||||
import { postChargeStripe } from '../../utils/ajax';
|
||||
import { userSelector } from '../../redux';
|
||||
|
||||
const propTypes = {
|
||||
@ -26,19 +25,14 @@ const propTypes = {
|
||||
getDonationButtonLabel: PropTypes.func.isRequired,
|
||||
handleProcessing: PropTypes.func,
|
||||
isSignedIn: PropTypes.bool,
|
||||
onDonationStateChange: PropTypes.func,
|
||||
postChargeStripe: PropTypes.func,
|
||||
showCloseBtn: PropTypes.func,
|
||||
stripe: PropTypes.shape({
|
||||
createToken: PropTypes.func.isRequired
|
||||
}),
|
||||
theme: PropTypes.string
|
||||
};
|
||||
const initialState = {
|
||||
donationState: {
|
||||
processing: false,
|
||||
success: false,
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
@ -50,9 +44,6 @@ class DonateFormChildViewForHOC extends Component {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
...initialState,
|
||||
donationAmount: this.props.donationAmount,
|
||||
donationDuration: this.props.donationDuration,
|
||||
isSubmissionValid: null,
|
||||
email: null,
|
||||
isEmailValid: true,
|
||||
@ -63,7 +54,6 @@ class DonateFormChildViewForHOC extends Component {
|
||||
this.handleEmailChange = this.handleEmailChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.postDonation = this.postDonation.bind(this);
|
||||
this.resetDonation = this.resetDonation.bind(this);
|
||||
this.handleEmailBlur = this.handleEmailBlur.bind(this);
|
||||
}
|
||||
|
||||
@ -106,41 +96,26 @@ class DonateFormChildViewForHOC extends Component {
|
||||
|
||||
const email = this.getUserEmail();
|
||||
if (!email || !isEmail(email)) {
|
||||
return this.setState(state => ({
|
||||
...state,
|
||||
donationState: {
|
||||
...state.donationState,
|
||||
error:
|
||||
'We need a valid email address to which we can send your' +
|
||||
' donation tax receipt.'
|
||||
}
|
||||
}));
|
||||
return this.props.onDonationStateChange({
|
||||
error:
|
||||
'We need a valid email address to which we can send your' +
|
||||
' donation tax receipt.'
|
||||
});
|
||||
}
|
||||
return this.props.stripe.createToken({ email }).then(({ error, token }) => {
|
||||
if (error) {
|
||||
return this.setState(state => ({
|
||||
...state,
|
||||
donationState: {
|
||||
...state.donationState,
|
||||
error:
|
||||
'Something went wrong processing your donation. Your card' +
|
||||
' has not been charged.'
|
||||
}
|
||||
}));
|
||||
return this.props.onDonationStateChange({
|
||||
error:
|
||||
'Something went wrong processing your donation. Your card' +
|
||||
' has not been charged.'
|
||||
});
|
||||
}
|
||||
return this.postDonation(token);
|
||||
});
|
||||
}
|
||||
|
||||
postDonation(token) {
|
||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
donationState: {
|
||||
...state.donationState,
|
||||
processing: true
|
||||
}
|
||||
}));
|
||||
const { donationAmount: amount, donationDuration: duration } = this.props;
|
||||
|
||||
// scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
@ -150,50 +125,15 @@ class DonateFormChildViewForHOC extends Component {
|
||||
if (this.props.handleProcessing) {
|
||||
this.props.handleProcessing(
|
||||
this.state.donationDuration,
|
||||
Math.round(this.state.donationAmount / 100)
|
||||
Math.round(amount / 100)
|
||||
);
|
||||
}
|
||||
|
||||
return postChargeStripe({
|
||||
return this.props.postChargeStripe({
|
||||
token,
|
||||
amount,
|
||||
duration
|
||||
})
|
||||
.then(response => {
|
||||
const data = response && response.data;
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
donationState: {
|
||||
...state.donationState,
|
||||
processing: false,
|
||||
success: true,
|
||||
error: data.error ? data.error : null
|
||||
}
|
||||
}));
|
||||
})
|
||||
.catch(error => {
|
||||
const data =
|
||||
error.response && error.response.data
|
||||
? error.response.data
|
||||
: {
|
||||
error:
|
||||
'Something is not right. ' +
|
||||
'Please contact donors@freecodecamp.org.'
|
||||
};
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
donationState: {
|
||||
...state.donationState,
|
||||
processing: false,
|
||||
success: false,
|
||||
error: data.error
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
resetDonation() {
|
||||
return this.setState({ ...initialState });
|
||||
});
|
||||
}
|
||||
|
||||
renderCompletion(props) {
|
||||
@ -267,25 +207,13 @@ class DonateFormChildViewForHOC extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ donationAmount, donationDuration, email }) {
|
||||
this.setState({ donationAmount, donationDuration });
|
||||
componentWillReceiveProps({ email }) {
|
||||
if (this.state.email === null && email) {
|
||||
this.setState({ email });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
donationState: { processing, success, error }
|
||||
} = this.state;
|
||||
if (processing || success || error) {
|
||||
return this.renderCompletion({
|
||||
processing,
|
||||
success,
|
||||
error,
|
||||
reset: this.resetDonation
|
||||
});
|
||||
}
|
||||
return this.renderDonateForm();
|
||||
}
|
||||
}
|
||||
|
@ -348,3 +348,7 @@ li.disabled > a {
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
@ -9,8 +9,8 @@ import { Spacer } from '../helpers';
|
||||
import { blockNameify } from '../../../../utils/block-nameify';
|
||||
import Heart from '../../assets/icons/Heart';
|
||||
import Cup from '../../assets/icons/Cup';
|
||||
import MinimalDonateForm from './MinimalDonateForm';
|
||||
import { modalDefaultStateConfig } from '../../../../config/donation-settings';
|
||||
import DonateForm from './DonateForm';
|
||||
import { modalDefaultDonation } from '../../../../config/donation-settings';
|
||||
|
||||
import {
|
||||
closeDonationModal,
|
||||
@ -77,19 +77,21 @@ function DonateModal({
|
||||
setCloseLabel(true);
|
||||
};
|
||||
|
||||
if (show) {
|
||||
executeGA({ type: 'modal', data: '/donation-modal' });
|
||||
executeGA({
|
||||
type: 'event',
|
||||
data: {
|
||||
category: 'Donation',
|
||||
action: `Displayed ${
|
||||
isBlockDonation ? 'block' : 'progress'
|
||||
} donation modal`,
|
||||
nonInteraction: true
|
||||
}
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
executeGA({ type: 'modal', data: '/donation-modal' });
|
||||
executeGA({
|
||||
type: 'event',
|
||||
data: {
|
||||
category: 'Donation',
|
||||
action: `Displayed ${
|
||||
isBlockDonation ? 'block' : 'progress'
|
||||
} donation modal`,
|
||||
nonInteraction: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [show, isBlockDonation, executeGA]);
|
||||
|
||||
const durationToText = donationDuration => {
|
||||
if (donationDuration === 'onetime') return 'a one-time';
|
||||
@ -100,8 +102,8 @@ function DonateModal({
|
||||
|
||||
const donationText = (
|
||||
<b>
|
||||
Become {durationToText(modalDefaultStateConfig.donationDuration)}{' '}
|
||||
supporter of our nonprofit.
|
||||
Become {durationToText(modalDefaultDonation.donationDuration)} supporter
|
||||
of our nonprofit.
|
||||
</b>
|
||||
);
|
||||
const blockDonationText = (
|
||||
@ -141,7 +143,7 @@ function DonateModal({
|
||||
<Modal.Body>
|
||||
{isBlockDonation ? blockDonationText : progressDonationText}
|
||||
<Spacer />
|
||||
<MinimalDonateForm handleProcessing={handleProcessing} />
|
||||
<DonateForm handleProcessing={handleProcessing} isMinimalForm={true} />
|
||||
<Spacer />
|
||||
<Row>
|
||||
<Col sm={4} smOffset={4} xs={8} xsOffset={2}>
|
||||
|
@ -1,174 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Row, Col } from '@freecodecamp/react-bootstrap';
|
||||
import { StripeProvider, Elements } from 'react-stripe-elements';
|
||||
|
||||
import {
|
||||
amountsConfig,
|
||||
durationsConfig,
|
||||
modalDefaultStateConfig
|
||||
} from '../../../../config/donation-settings';
|
||||
import { stripePublicKey } from '../../../../config/env.json';
|
||||
import { stripeScriptLoader } from '../../utils/scriptLoaders';
|
||||
import DonateFormChildViewForHOC from './DonateFormChildViewForHOC';
|
||||
import DonateCompletion from './DonateCompletion';
|
||||
import PaypalButton from './PaypalButton';
|
||||
import { userSelector } from '../../redux';
|
||||
|
||||
import { Spacer } from '../../components/helpers';
|
||||
|
||||
import './Donation.css';
|
||||
|
||||
const propTypes = {
|
||||
defaultTheme: PropTypes.string,
|
||||
handleProcessing: PropTypes.func,
|
||||
isDonating: PropTypes.bool,
|
||||
stripe: PropTypes.shape({
|
||||
createToken: PropTypes.func.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
({ isDonating }) => ({
|
||||
isDonating
|
||||
})
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
donationState: {
|
||||
processing: false,
|
||||
success: false,
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
|
||||
class MinimalDonateForm extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.durations = durationsConfig;
|
||||
this.amounts = amountsConfig;
|
||||
|
||||
this.state = {
|
||||
...modalDefaultStateConfig,
|
||||
...initialState,
|
||||
isDonating: this.props.isDonating,
|
||||
stripe: null
|
||||
};
|
||||
this.handleStripeLoad = this.handleStripeLoad.bind(this);
|
||||
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
||||
this.resetDonation = this.resetDonation.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
|
||||
if (stripePublicKey) {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
stripe: window.Stripe(stripePublicKey)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
resetDonation() {
|
||||
return this.setState({ ...initialState });
|
||||
}
|
||||
|
||||
onDonationStateChange(success, processing, error) {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
donationState: {
|
||||
...state.donationState,
|
||||
processing: processing,
|
||||
success: success,
|
||||
error: error
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
renderCompletion(props) {
|
||||
return <DonateCompletion {...props} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { donationAmount, donationDuration, stripe } = this.state;
|
||||
const { handleProcessing, defaultTheme } = this.props;
|
||||
const {
|
||||
donationState: { processing, success, error }
|
||||
} = this.state;
|
||||
|
||||
const donationPlan = `$${donationAmount / 100} / ${donationDuration}`;
|
||||
if (processing || success || error) {
|
||||
return this.renderCompletion({
|
||||
processing,
|
||||
success,
|
||||
error,
|
||||
reset: this.resetDonation
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<b>Confirm your donation of {donationPlan} with PayPal:</b>
|
||||
<Spacer />
|
||||
<PaypalButton
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
handleProcessing={handleProcessing}
|
||||
onDonationStateChange={this.onDonationStateChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<b>Or donate with a credit card:</b>
|
||||
<Spacer />
|
||||
<StripeProvider stripe={stripe}>
|
||||
<Elements>
|
||||
<DonateFormChildViewForHOC
|
||||
defaultTheme={defaultTheme}
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
getDonationButtonLabel={() =>
|
||||
`Confirm your donation of ${donationPlan}`
|
||||
}
|
||||
handleProcessing={handleProcessing}
|
||||
/>
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MinimalDonateForm.displayName = 'MinimalDonateForm';
|
||||
MinimalDonateForm.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(MinimalDonateForm);
|
@ -6,7 +6,6 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import PayPalButtonScriptLoader from './PayPalButtonScriptLoader';
|
||||
import { paypalClientId, deploymentEnv } from '../../../config/env.json';
|
||||
import { verifySubscriptionPaypal } from '../../utils/ajax';
|
||||
import {
|
||||
paypalConfigurator,
|
||||
paypalConfigTypes
|
||||
@ -38,39 +37,20 @@ export class PaypalButton extends Component {
|
||||
handleApproval = (data, isSubscription) => {
|
||||
const { amount, duration } = this.state;
|
||||
const { skipAddDonation = false } = this.props;
|
||||
|
||||
// Skip the api if user is not signed in or if its a one-time donation
|
||||
if (skipAddDonation || !isSubscription) {
|
||||
this.props.onDonationStateChange(
|
||||
true,
|
||||
false,
|
||||
data.error ? data.error : ''
|
||||
);
|
||||
} else {
|
||||
this.props.handleProcessing(
|
||||
duration,
|
||||
amount,
|
||||
'Paypal payment submission'
|
||||
);
|
||||
this.props.onDonationStateChange(false, true, '');
|
||||
verifySubscriptionPaypal(data)
|
||||
.then(response => {
|
||||
const data = response && response.data;
|
||||
this.props.onDonationStateChange(
|
||||
true,
|
||||
false,
|
||||
data.error ? data.error : ''
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
const data =
|
||||
error.response && error.response.data
|
||||
? error.response.data
|
||||
: {
|
||||
error: `Something is not right. Please contact team@freecodecamp.org`
|
||||
};
|
||||
this.props.onDonationStateChange(false, false, data.error);
|
||||
});
|
||||
if (!skipAddDonation || isSubscription) {
|
||||
this.props.addDonation(data);
|
||||
}
|
||||
|
||||
this.props.handleProcessing(duration, amount, 'Paypal payment submission');
|
||||
|
||||
// Show success anytime because the payment has gone through paypal
|
||||
this.props.onDonationStateChange({
|
||||
processing: false,
|
||||
success: true,
|
||||
error: data.error ? data.error : null
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -107,14 +87,18 @@ export class PaypalButton extends Component {
|
||||
this.handleApproval(data, isSubscription);
|
||||
}}
|
||||
onCancel={() => {
|
||||
this.props.onDonationStateChange(
|
||||
false,
|
||||
false,
|
||||
`Uh - oh. It looks like your transaction didn't go through. Could you please try again?`
|
||||
);
|
||||
this.props.onDonationStateChange({
|
||||
processing: false,
|
||||
success: false,
|
||||
error: `Uh - oh. It looks like your transaction didn't go through. Could you please try again?`
|
||||
});
|
||||
}}
|
||||
onError={() =>
|
||||
this.props.onDonationStateChange(false, false, 'Please try again.')
|
||||
this.props.onDonationStateChange({
|
||||
processing: false,
|
||||
success: false,
|
||||
error: 'Please try again.'
|
||||
})
|
||||
}
|
||||
plantId={planId}
|
||||
style={{
|
||||
@ -127,6 +111,7 @@ export class PaypalButton extends Component {
|
||||
}
|
||||
|
||||
const propTypes = {
|
||||
addDonation: PropTypes.func,
|
||||
donationAmount: PropTypes.number,
|
||||
donationDuration: PropTypes.string,
|
||||
handleProcessing: PropTypes.func,
|
||||
|
@ -6,12 +6,10 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Grid, Row, Col, Alert } from '@freecodecamp/react-bootstrap';
|
||||
|
||||
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, executeGA } from '../redux';
|
||||
import { stripeScriptLoader } from '../utils/scriptLoaders';
|
||||
|
||||
const propTypes = {
|
||||
executeGA: PropTypes.func,
|
||||
@ -40,11 +38,9 @@ export class DonatePage extends Component {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.state = {
|
||||
stripe: null,
|
||||
enableSettings: false
|
||||
};
|
||||
this.handleProcessing = this.handleProcessing.bind(this);
|
||||
this.handleStripeLoad = this.handleStripeLoad.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -56,47 +52,21 @@ export class DonatePage extends Component {
|
||||
nonInteraction: true
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
handleProcessing(duration, amount) {
|
||||
handleProcessing(duration, amount, action = 'stripe button click') {
|
||||
this.props.executeGA({
|
||||
type: 'event',
|
||||
data: {
|
||||
category: 'donation',
|
||||
action: 'donate page stripe form submission',
|
||||
action: `donate page ${action}`,
|
||||
label: duration,
|
||||
value: amount
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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, isDonating } = this.props;
|
||||
|
||||
if (showLoading) {
|
||||
@ -141,7 +111,6 @@ export class DonatePage extends Component {
|
||||
<DonateForm
|
||||
enableDonationSettingsPage={this.enableDonationSettingsPage}
|
||||
handleProcessing={this.handleProcessing}
|
||||
stripe={stripe}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
|
@ -1,13 +1,28 @@
|
||||
import { put, select, takeEvery, delay } from 'redux-saga/effects';
|
||||
import {
|
||||
put,
|
||||
select,
|
||||
takeEvery,
|
||||
takeLeading,
|
||||
delay,
|
||||
call
|
||||
} from 'redux-saga/effects';
|
||||
|
||||
import {
|
||||
openDonationModal,
|
||||
preventBlockDonationRequests,
|
||||
shouldRequestDonationSelector,
|
||||
preventProgressDonationRequests,
|
||||
canRequestBlockDonationSelector
|
||||
canRequestBlockDonationSelector,
|
||||
addDonationComplete,
|
||||
addDonationError,
|
||||
postChargeStripeComplete,
|
||||
postChargeStripeError
|
||||
} from './';
|
||||
|
||||
import { addDonation, postChargeStripe } from '../utils/ajax';
|
||||
|
||||
const defaultDonationError = `Something is not right. Please contact donors@freecodecamp.org`;
|
||||
|
||||
function* showDonateModalSaga() {
|
||||
let shouldRequestDonation = yield select(shouldRequestDonationSelector);
|
||||
if (shouldRequestDonation) {
|
||||
@ -22,6 +37,38 @@ function* showDonateModalSaga() {
|
||||
}
|
||||
}
|
||||
|
||||
export function createDonationSaga(types) {
|
||||
return [takeEvery(types.tryToShowDonationModal, showDonateModalSaga)];
|
||||
function* addDonationSaga({ payload }) {
|
||||
try {
|
||||
yield call(addDonation, payload);
|
||||
yield put(addDonationComplete());
|
||||
} catch (error) {
|
||||
const data =
|
||||
error.response && error.response.data
|
||||
? error.response.data
|
||||
: {
|
||||
message: defaultDonationError
|
||||
};
|
||||
yield put(addDonationError(data.message));
|
||||
}
|
||||
}
|
||||
|
||||
function* postChargeStripeSaga({ payload }) {
|
||||
try {
|
||||
yield call(postChargeStripe, payload);
|
||||
yield put(postChargeStripeComplete());
|
||||
} catch (error) {
|
||||
const err =
|
||||
error.response && error.response.data
|
||||
? error.response.data.error
|
||||
: defaultDonationError;
|
||||
yield put(postChargeStripeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
export function createDonationSaga(types) {
|
||||
return [
|
||||
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
|
||||
takeEvery(types.addDonation, addDonationSaga),
|
||||
takeLeading(types.postChargeStripe, postChargeStripeSaga)
|
||||
];
|
||||
}
|
||||
|
@ -31,6 +31,12 @@ export const defaultFetchState = {
|
||||
error: null
|
||||
};
|
||||
|
||||
export const defaultDonationFormState = {
|
||||
processing: false,
|
||||
success: false,
|
||||
error: ''
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
appUsername: '',
|
||||
canRequestBlockDonation: false,
|
||||
@ -51,7 +57,10 @@ const initialState = {
|
||||
sessionMeta: { activeDonations: 0 },
|
||||
showDonationModal: false,
|
||||
isBlockDonationModal: false,
|
||||
isOnline: true
|
||||
isOnline: true,
|
||||
donationFormState: {
|
||||
...defaultDonationFormState
|
||||
}
|
||||
};
|
||||
|
||||
export const types = createTypes(
|
||||
@ -71,7 +80,10 @@ export const types = createTypes(
|
||||
'updateComplete',
|
||||
'updateCurrentChallengeId',
|
||||
'updateFailed',
|
||||
'updateDonationFormState',
|
||||
...createAsyncTypes('fetchUser'),
|
||||
...createAsyncTypes('addDonation'),
|
||||
...createAsyncTypes('postChargeStripe'),
|
||||
...createAsyncTypes('fetchProfileForUser'),
|
||||
...createAsyncTypes('acceptTerms'),
|
||||
...createAsyncTypes('showCert'),
|
||||
@ -112,6 +124,9 @@ export const preventBlockDonationRequests = createAction(
|
||||
export const preventProgressDonationRequests = createAction(
|
||||
types.preventProgressDonationRequests
|
||||
);
|
||||
export const updateDonationFormState = createAction(
|
||||
types.updateDonationFormState
|
||||
);
|
||||
|
||||
export const onlineStatusChange = createAction(types.onlineStatusChange);
|
||||
|
||||
@ -133,6 +148,16 @@ export const fetchUser = createAction(types.fetchUser);
|
||||
export const fetchUserComplete = createAction(types.fetchUserComplete);
|
||||
export const fetchUserError = createAction(types.fetchUserError);
|
||||
|
||||
export const addDonation = createAction(types.addDonation);
|
||||
export const addDonationComplete = createAction(types.addDonationComplete);
|
||||
export const addDonationError = createAction(types.addDonationError);
|
||||
|
||||
export const postChargeStripe = createAction(types.postChargeStripe);
|
||||
export const postChargeStripeComplete = createAction(
|
||||
types.postChargeStripeComplete
|
||||
);
|
||||
export const postChargeStripeError = createAction(types.postChargeStripeError);
|
||||
|
||||
export const fetchProfileForUser = createAction(types.fetchProfileForUser);
|
||||
export const fetchProfileForUserComplete = createAction(
|
||||
types.fetchProfileForUserComplete
|
||||
@ -160,7 +185,6 @@ export const completedChallengesSelector = state =>
|
||||
export const completionCountSelector = state => state[ns].completionCount;
|
||||
export const currentChallengeIdSelector = state => state[ns].currentChallengeId;
|
||||
export const isDonatingSelector = state => userSelector(state).isDonating;
|
||||
|
||||
export const isOnlineSelector = state => state[ns].isOnline;
|
||||
export const isSignedInSelector = state => !!state[ns].appUsername;
|
||||
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
|
||||
@ -168,12 +192,11 @@ export const canRequestBlockDonationSelector = state =>
|
||||
state[ns].canRequestBlockDonation;
|
||||
export const isBlockDonationModalSelector = state =>
|
||||
state[ns].isBlockDonationModal;
|
||||
|
||||
export const donationFormStateSelector = state => state[ns].donationFormState;
|
||||
export const signInLoadingSelector = state =>
|
||||
userFetchStateSelector(state).pending;
|
||||
export const showCertSelector = state => state[ns].showCert;
|
||||
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
|
||||
|
||||
export const shouldRequestDonationSelector = state => {
|
||||
const completedChallenges = completedChallengesSelector(state);
|
||||
const completionCount = completionCountSelector(state);
|
||||
@ -389,6 +412,56 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
canRequestBlockDonation: true
|
||||
}),
|
||||
[types.updateDonationFormState]: (state, { payload }) => ({
|
||||
...state,
|
||||
donationFormState: { ...state.donationFormState, ...payload }
|
||||
}),
|
||||
[types.addDonation]: state => ({
|
||||
...state,
|
||||
donationFormState: { ...defaultDonationFormState, processing: true }
|
||||
}),
|
||||
[types.addDonationComplete]: state => {
|
||||
const { appUsername } = state;
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[appUsername]: {
|
||||
...state.user[appUsername],
|
||||
isDonating: true
|
||||
}
|
||||
},
|
||||
|
||||
donationFormState: { ...defaultDonationFormState, success: true }
|
||||
};
|
||||
},
|
||||
[types.addDonationError]: (state, { payload }) => ({
|
||||
...state,
|
||||
donationFormState: { ...defaultDonationFormState, error: payload }
|
||||
}),
|
||||
[types.postChargeStripe]: state => ({
|
||||
...state,
|
||||
donationFormState: { ...defaultDonationFormState, processing: true }
|
||||
}),
|
||||
[types.postChargeStripeComplete]: state => {
|
||||
const { appUsername } = state;
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[appUsername]: {
|
||||
...state.user[appUsername],
|
||||
isDonating: true
|
||||
}
|
||||
},
|
||||
|
||||
donationFormState: { ...defaultDonationFormState, success: true }
|
||||
};
|
||||
},
|
||||
[types.postChargeStripeError]: (state, { payload }) => ({
|
||||
...state,
|
||||
donationFormState: { ...defaultDonationFormState, error: payload }
|
||||
}),
|
||||
[types.fetchUser]: state => ({
|
||||
...state,
|
||||
userFetchState: { ...defaultFetchState }
|
||||
|
@ -62,7 +62,7 @@ export function postChargeStripe(body) {
|
||||
return post('/donate/charge-stripe', body);
|
||||
}
|
||||
|
||||
export function verifySubscriptionPaypal(body) {
|
||||
export function addDonation(body) {
|
||||
return post('/donate/add-donation', body);
|
||||
}
|
||||
|
||||
|
@ -14,11 +14,11 @@ const defaultAmount = {
|
||||
month: 500,
|
||||
onetime: 25000
|
||||
};
|
||||
const defaultStateConfig = {
|
||||
const defaultDonation = {
|
||||
donationAmount: defaultAmount['month'],
|
||||
donationDuration: 'month'
|
||||
};
|
||||
const modalDefaultStateConfig = {
|
||||
const modalDefaultDonation = {
|
||||
donationAmount: 500,
|
||||
donationDuration: 'month'
|
||||
};
|
||||
@ -123,11 +123,11 @@ module.exports = {
|
||||
durationsConfig,
|
||||
amountsConfig,
|
||||
defaultAmount,
|
||||
defaultStateConfig,
|
||||
defaultDonation,
|
||||
durationKeysConfig,
|
||||
donationOneTimeConfig,
|
||||
donationSubscriptionConfig,
|
||||
modalDefaultStateConfig,
|
||||
modalDefaultDonation,
|
||||
onetimeSKUConfig,
|
||||
paypalConfigTypes,
|
||||
paypalConfigurator,
|
||||
|
Reference in New Issue
Block a user