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