fix(client): unify client donations methods (#39562)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2020-10-14 13:23:26 +03:00
committed by GitHub
parent 1d2ff7aef6
commit 3106fe804f
12 changed files with 377 additions and 417 deletions

View File

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

View File

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

View File

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

View File

@ -348,3 +348,7 @@ li.disabled > a {
justify-content: center;
align-content: center;
}
.hide {
display: none;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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