fix(client): unify client donations methods (#39562)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
Reference in New Issue
Block a user