feat(donate): PayPal integration
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
e3db423abf
commit
6c6eadfbe4
42
client/package-lock.json
generated
42
client/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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 = (
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
112
client/src/components/Donation/PaypalButton.js
Normal file
112
client/src/components/Donation/PaypalButton.js
Normal 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);
|
@@ -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;
|
||||
|
@@ -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}>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global describe it expect */
|
||||
/* global expect */
|
||||
import { isObject } from 'lodash';
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
|
Reference in New Issue
Block a user