diff --git a/client/src/components/Donation/DonateForm.js b/client/src/components/Donation/DonateForm.js
index 4703ebfd4e..5d5e6bd6e8 100644
--- a/client/src/components/Donation/DonateForm.js
+++ b/client/src/components/Donation/DonateForm.js
@@ -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')}
>
Donate with Apple Pay
@@ -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')}
>
Donate with Google Pay
@@ -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')}
>
Donate with Card
@@ -292,6 +346,7 @@ class DonateForm extends Component {
;
}
- 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 (
+
+
+
+ {this.getDonationButtonLabel()} with PayPal:
+
+
+
+
+
+ Or donate with a credit card:
+
+
+
+
+
+
+
+
+ );
+ }
+
+ renderPageForm() {
return (
@@ -335,9 +424,45 @@ class DonateForm extends Component {
);
}
+
+ 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
+ })}
+
+ {isMinimalForm
+ ? this.renderModalForm(processing)
+ : this.renderPageForm(processing)}
+
+ >
+ );
+ }
}
DonateForm.displayName = 'DonateForm';
DonateForm.propTypes = propTypes;
-export default connect(mapStateToProps)(DonateForm);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(DonateForm);
diff --git a/client/src/components/Donation/DonateFormChildViewForHOC.js b/client/src/components/Donation/DonateFormChildViewForHOC.js
index 2df8259ddd..223a799462 100644
--- a/client/src/components/Donation/DonateFormChildViewForHOC.js
+++ b/client/src/components/Donation/DonateFormChildViewForHOC.js
@@ -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();
}
}
diff --git a/client/src/components/Donation/Donation.css b/client/src/components/Donation/Donation.css
index 90a84f3e68..8f49fc6648 100644
--- a/client/src/components/Donation/Donation.css
+++ b/client/src/components/Donation/Donation.css
@@ -348,3 +348,7 @@ li.disabled > a {
justify-content: center;
align-content: center;
}
+
+.hide {
+ display: none;
+}
diff --git a/client/src/components/Donation/DonationModal.js b/client/src/components/Donation/DonationModal.js
index a8dfe8d1c9..aa3e9412b6 100644
--- a/client/src/components/Donation/DonationModal.js
+++ b/client/src/components/Donation/DonationModal.js
@@ -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 = (
- Become {durationToText(modalDefaultStateConfig.donationDuration)}{' '}
- supporter of our nonprofit.
+ Become {durationToText(modalDefaultDonation.donationDuration)} supporter
+ of our nonprofit.
);
const blockDonationText = (
@@ -141,7 +143,7 @@ function DonateModal({
{isBlockDonation ? blockDonationText : progressDonationText}
-
+
diff --git a/client/src/components/Donation/MinimalDonateForm.js b/client/src/components/Donation/MinimalDonateForm.js
deleted file mode 100644
index 5a58e78808..0000000000
--- a/client/src/components/Donation/MinimalDonateForm.js
+++ /dev/null
@@ -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 ;
- }
-
- 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 (
-
-
-
- Confirm your donation of {donationPlan} with PayPal:
-
-
-
-
-
- Or donate with a credit card:
-
-
-
-
- `Confirm your donation of ${donationPlan}`
- }
- handleProcessing={handleProcessing}
- />
-
-
-
-
- );
- }
-}
-
-MinimalDonateForm.displayName = 'MinimalDonateForm';
-MinimalDonateForm.propTypes = propTypes;
-
-export default connect(
- mapStateToProps,
- null
-)(MinimalDonateForm);
diff --git a/client/src/components/Donation/PaypalButton.js b/client/src/components/Donation/PaypalButton.js
index 97ffabc25b..a0c351606e 100644
--- a/client/src/components/Donation/PaypalButton.js
+++ b/client/src/components/Donation/PaypalButton.js
@@ -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,
diff --git a/client/src/pages/donate.js b/client/src/pages/donate.js
index 04115bdb3d..c6b6a7a510 100644
--- a/client/src/pages/donate.js
+++ b/client/src/pages/donate.js
@@ -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 {
diff --git a/client/src/redux/donation-saga.js b/client/src/redux/donation-saga.js
index a8d3ef06a6..4d3fa981e4 100644
--- a/client/src/redux/donation-saga.js
+++ b/client/src/redux/donation-saga.js
@@ -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)
+ ];
}
diff --git a/client/src/redux/index.js b/client/src/redux/index.js
index 227c6f1b77..528bef8465 100644
--- a/client/src/redux/index.js
+++ b/client/src/redux/index.js
@@ -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 }
diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js
index 091a8c1f55..7f6931cdad 100644
--- a/client/src/utils/ajax.js
+++ b/client/src/utils/ajax.js
@@ -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);
}
diff --git a/config/donation-settings.js b/config/donation-settings.js
index 5d17d4e3ac..d4d979d9b0 100644
--- a/config/donation-settings.js
+++ b/config/donation-settings.js
@@ -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,