feat(donate): PayPal integration

This commit is contained in:
Ahmad Abdolsaheb
2020-03-13 12:25:57 +03:00
committed by Mrugesh Mohapatra
parent e3db423abf
commit 6c6eadfbe4
24 changed files with 1040 additions and 70 deletions

View File

@@ -18944,6 +18944,48 @@
}
}
},
"react-paypal-button-v2": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/react-paypal-button-v2/-/react-paypal-button-v2-2.6.1.tgz",
"integrity": "sha512-ia1zzdRgziSfnJ/UueM6i0omEocbv4cge3mBRmpA6WSyxsX6c501HWHVwvXMEbTDCPK5+0ZKewhBzf/wq618Cg==",
"requires": {
"prop-types": "^15.7.2",
"rimraf": "^3.0.0"
},
"dependencies": {
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
}
}
},
"react-prop-types": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/react-prop-types/-/react-prop-types-0.4.0.tgz",

View File

@@ -56,6 +56,7 @@
"react-identicons": "^1.1.7",
"react-instantsearch-dom": "^6.0.0-beta.0",
"react-monaco-editor": "^0.31.0",
"react-paypal-button-v2": "^2.6.1",
"react-redux": "^5.0.7",
"react-reflex": "^3.0.18",
"react-responsive": "^6.1.1",

View File

@@ -137,12 +137,12 @@ class ShowCertification extends Component {
this.setState({ isDonationDisplayed: false, isDonationClosed: true });
}
handleProcessing(duration, amount) {
handleProcessing(duration, amount, action = 'stripe form submission') {
this.props.executeGA({
type: 'event',
data: {
category: 'donation',
action: 'certificate stripe form submission',
action: `certificate ${action}`,
label: duration,
value: amount
}

View File

@@ -212,6 +212,10 @@ li.disabled > a {
}
}
.donation-modal {
font-family: 'Lato', sans-serif;
}
.donation-modal .btn-link:focus {
outline-width: 1px;
outline-style: solid;

View File

@@ -59,12 +59,16 @@ function DonateModal({
executeGA
}) {
const [closeLabel, setCloseLabel] = React.useState(false);
const handleProcessing = (duration, amount) => {
const handleProcessing = (
duration,
amount,
action = 'stripe form submission'
) => {
executeGA({
type: 'event',
data: {
category: 'donation',
action: 'Modal strip form submission',
action: `Modal ${action}`,
label: duration,
value: amount
}
@@ -88,8 +92,8 @@ function DonateModal({
const donationText = (
<b>
Become a supporter and help us create even more learning resources for
you.
Become a $5 / month supporter and help us create even more learning
resources for you and your family.
</b>
);
const blockDonationText = (

View File

@@ -13,8 +13,12 @@ import {
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 = {
@@ -33,6 +37,14 @@ const mapStateToProps = createSelector(
})
);
const initialState = {
donationState: {
processing: false,
success: false,
error: ''
}
};
class MinimalDonateForm extends Component {
constructor(...args) {
super(...args);
@@ -42,10 +54,13 @@ class MinimalDonateForm extends Component {
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() {
@@ -77,12 +92,54 @@ class MinimalDonateForm extends Component {
}
}
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;
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}>
<PaypalButton
handleProcessing={handleProcessing}
onDonationStateChange={this.onDonationStateChange}
/>
</Col>
<Col sm={10} smOffset={1} xs={12}>
<Spacer />
<b>Or donate with credit card number.</b>
<Spacer />
</Col>
<Col sm={10} smOffset={1} xs={12}>
<StripeProvider stripe={stripe}>
<Elements>
@@ -91,7 +148,7 @@ class MinimalDonateForm extends Component {
donationAmount={donationAmount}
donationDuration={donationDuration}
getDonationButtonLabel={() =>
`Confirm your donation of $5 per month`
`Confirm your donation of $5 / month`
}
handleProcessing={handleProcessing}
/>

View File

@@ -0,0 +1,112 @@
/* eslint-disable camelcase */
/* global ENVIRONMENT */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { PayPalButton } from 'react-paypal-button-v2';
import { paypalClientId } from '../../../config/env.json';
import { verifySubscriptionPaypal } from '../../utils/ajax';
import { paypalConfig } from '../../../../config/donation-settings';
import { signInLoadingSelector, userSelector, executeGA } from '../../redux';
const paypalDurationPlans =
ENVIRONMENT === 'production'
? paypalConfig.production.durationPlans
: paypalConfig.development.durationPlans;
export class PaypalButton extends Component {
constructor(...props) {
super(...props);
this.state = {
planId: paypalDurationPlans.month['500'].planId
};
this.handleApproval = this.handleApproval.bind(this);
}
handleApproval = data => {
this.props.handleProcessing('month', 500, '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);
});
};
render() {
return (
<PayPalButton
createSubscription={(data, actions) => {
executeGA({
type: 'event',
data: {
category: 'Donation',
action: `Modal Paypal clicked`
}
});
return actions.subscription.create({
plan_id: this.state.planId
});
}}
onApprove={data => {
this.handleApproval(data);
}}
onCancel={() => {
this.props.onDonationStateChange(
false,
false,
'Payment has been canceled.'
);
}}
onError={() =>
this.props.onDonationStateChange(false, false, 'Please try again.')
}
options={{
vault: true,
disableFunding: 'card',
clientId: paypalClientId
}}
style={{
tagline: false,
height: 43
}}
/>
);
}
}
const propTypes = {
handleProcessing: PropTypes.func,
isDonating: PropTypes.bool,
onDonationStateChange: PropTypes.func
};
const mapStateToProps = createSelector(
userSelector,
signInLoadingSelector,
({ isDonating }, showLoading) => ({
isDonating,
showLoading
})
);
PaypalButton.displayName = 'PaypalButton';
PaypalButton.propTypes = propTypes;
export default connect(mapStateToProps)(PaypalButton);

View File

@@ -42,6 +42,10 @@
padding: 20px 0px;
}
.certificate-outer-wrapper .donation-section hr {
border: 1px solid var(--gray-10);
}
.certificate-outer-wrapper .donation-completion .btn {
background-color: var(--gray-15);
border-color: var(--gray-85);
@@ -61,6 +65,11 @@
color: var(--quaternary-color);
}
.certificate-outer-wrapper .donation-section,
.certificate-outer-wrapper .donation-section p {
font-family: 'Lato', sans-serif;
}
.certification-namespace header {
width: 100%;
height: 140px;

View File

@@ -12,6 +12,7 @@ import DonateForm from '../components/Donation/DonateForm';
import DonateText from '../components/Donation/DonateText';
import { signInLoadingSelector, userSelector, executeGA } from '../redux';
import { stripeScriptLoader } from '../utils/scriptLoaders';
import { PaypalButton } from '../components/Donation/PaypalButton';
const propTypes = {
executeGA: PropTypes.func,
@@ -118,6 +119,9 @@ export class DonatePage extends Component {
<Spacer />
</Col>
</Row>
<Row>
<PaypalButton />
</Row>
<Row>
{isDonating ? (
<Col md={6} mdOffset={3}>

View File

@@ -54,6 +54,10 @@ export function postChargeStripe(body) {
return post('/donate/charge-stripe', body);
}
export function verifySubscriptionPaypal(body) {
return post('/donate/add-donation', body);
}
export function postCreateHmacHash(body) {
return post(`/donate/create-hmac-hash`, body);
}

View File

@@ -1,4 +1,4 @@
/* global describe it expect */
/* global expect */
import { isObject } from 'lodash';
import sinon from 'sinon';
import {