feat(donate): integrate servicebot

This commit is contained in:
Mrugesh Mohapatra
2019-11-13 19:40:49 +05:30
parent 2cb8c16b28
commit aeec1bb9e6
12 changed files with 411 additions and 40 deletions

View File

@@ -42,7 +42,7 @@ function DonateCompletion({ processing, reset, success, error = null }) {
</p>
<p>
You can update your supporter status at any time from the 'manage
your existing donation' section on this page.
your existing donation' section below on this page.
</p>
</div>
)}

View File

@@ -30,6 +30,7 @@ const numToCommas = num =>
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
const propTypes = {
enableDonationSettingsPage: PropTypes.func.isRequired,
isDonating: PropTypes.bool,
isSignedIn: PropTypes.bool,
navigate: PropTypes.func.isRequired,
@@ -204,7 +205,7 @@ class DonateForm extends Component {
}
renderDonationOptions() {
const { stripe } = this.props;
const { stripe, enableDonationSettingsPage } = this.props;
const {
donationAmount,
donationDuration,
@@ -242,6 +243,7 @@ class DonateForm extends Component {
<DonateFormChildViewForHOC
donationAmount={donationAmount}
donationDuration={donationDuration}
enableDonationSettingsPage={enableDonationSettingsPage}
getDonationButtonLabel={this.getDonationButtonLabel}
hideAmountOptionsCB={this.hideAmountOptionsCB}
/>
@@ -288,18 +290,6 @@ class DonateForm extends Component {
this.renderDonationOptions()
)}
</Col>
<Col sm={10} smOffset={1} xs={12}>
<Spacer size={2} />
<h3 className='text-center'>Manage your existing donation</h3>
<Button block={true} bsStyle='primary' disabled={true}>
Update your existing donation
</Button>
<Spacer />
<Button block={true} bsStyle='primary' disabled={true}>
Download donation receipts
</Button>
<Spacer />
</Col>
</Row>
);
}

View File

@@ -21,6 +21,7 @@ const propTypes = {
donationAmount: PropTypes.number.isRequired,
donationDuration: PropTypes.string.isRequired,
email: PropTypes.string,
enableDonationSettingsPage: PropTypes.func.isRequired,
getDonationButtonLabel: PropTypes.func.isRequired,
hideAmountOptionsCB: PropTypes.func.isRequired,
isSignedIn: PropTypes.bool,
@@ -119,6 +120,7 @@ class DonateFormChildViewForHOC extends Component {
}
postDonation(token) {
const { enableDonationSettingsPage } = this.props;
const { donationAmount: amount, donationDuration: duration } = this.state;
this.setState(state => ({
...state,
@@ -144,6 +146,7 @@ class DonateFormChildViewForHOC extends Component {
error: data.error ? data.error : null
}
}));
enableDonationSettingsPage();
})
.catch(error => {
const data =

View File

@@ -0,0 +1,71 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { servicebotId } from '../../../../config/env.json';
import { servicebotScriptLoader } from '../../../utils/scriptLoaders';
import '../Donation.css';
const propTypes = {
email: PropTypes.string.isRequired,
hash: PropTypes.string.isRequired
};
export class DonationServicebotEmbed extends Component {
constructor(...props) {
super(...props);
this.state = {
email: this.props.email,
hash: this.props.hash
};
this.setServiceBotConfig = this.setServiceBotConfig.bind(this);
}
setServiceBotConfig() {
const { email, hash } = this.state;
/* eslint-disable camelcase */
window.servicebotSettings = {
type: 'portal',
servicebot_id: servicebotId,
service: 'freeCodeCamp.org',
email,
hash,
options: {
cancel_now: true,
disableCoupon: true,
forceCard: true,
disableTiers: [
'Monthly $10 Donation - Unavailable',
'Monthly $3 Donation - Unavailable'
],
card: {
hideName: true,
hideAddress: true,
hideCountryPostal: true
},
messageOnCancel: `Thanks again for supporting our tiny nonprofit. We are helping millions of people around the world learn to code for free. Please confirm: are you certain you want to stop your donation?`
}
};
/* eslint-enable camelcase */
}
componentDidMount() {
servicebotScriptLoader();
}
render() {
this.setServiceBotConfig();
return (
<div className='fcc-servicebot-embed-portal'>
<div id='servicebot-subscription-portal'></div>
</div>
);
}
}
DonationServicebotEmbed.displayName = 'DonationServicebotEmbed';
DonationServicebotEmbed.propTypes = propTypes;
export default DonationServicebotEmbed;

View File

@@ -3,32 +3,40 @@ import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
import { Grid, Row, Col, Button } from '@freecodecamp/react-bootstrap';
import { stripePublicKey } from '../../config/env.json';
import { Spacer, Loader } from '../components/helpers';
import DonateForm from '../components/Donation/components/DonateForm';
import DonateText from '../components/Donation/components/DonateText';
import { signInLoadingSelector } from '../redux';
import { signInLoadingSelector, userSelector } from '../redux';
import { stripeScriptLoader } from '../utils/scriptLoaders';
const propTypes = {
isDonating: PropTypes.bool,
showLoading: PropTypes.bool.isRequired
};
const mapStateToProps = createSelector(
userSelector,
signInLoadingSelector,
showLoading => ({
({ isDonating }, showLoading) => ({
isDonating,
showLoading
})
);
const propTypes = {
showLoading: PropTypes.bool.isRequired
};
export class DonatePage extends Component {
constructor(...props) {
super(...props);
this.state = {
stripe: null
stripe: null,
enableSettings: false
};
this.enableDonationSettingsPage = this.enableDonationSettingsPage.bind(
this
);
this.handleStripeLoad = this.handleStripeLoad.bind(this);
}
@@ -60,9 +68,14 @@ export class DonatePage extends Component {
}));
}
enableDonationSettingsPage(enableSettings = true) {
this.setState({ enableSettings });
}
render() {
const { stripe } = this.state;
const { showLoading } = this.props;
const { showLoading, isDonating } = this.props;
const { enableSettings } = this.state;
if (showLoading) {
return <Loader fullScreen={true} />;
@@ -81,7 +94,32 @@ export class DonatePage extends Component {
</Row>
<Row>
<Col md={6}>
<DonateForm stripe={stripe} />
<DonateForm
enableDonationSettingsPage={this.enableDonationSettingsPage}
stripe={stripe}
/>
<Row>
<Col sm={10} smOffset={1} xs={12}>
<Spacer size={2} />
<h3 className='text-center'>Manage your existing donation</h3>
{[
`Update your existing donation`,
`Download donation receipts`
].map(donationSettingOps => (
<div key={donationSettingOps}>
<Button
block={true}
bsStyle='primary'
disabled={!isDonating && !enableSettings}
href='/donation/settings'
>
{donationSettingOps}
</Button>
<Spacer />
</div>
))}
</Col>
</Row>
</Col>
<Col md={6}>
<DonateText />

View File

@@ -0,0 +1,209 @@
import React, { Component, Fragment } from 'react';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Grid, Row, Col, Button } from '@freecodecamp/react-bootstrap';
import { uniq } from 'lodash';
import { apiLocation } from '../../../config/env.json';
import { postCreateHmacHash } from '../../utils/ajax';
import {
signInLoadingSelector,
userSelector,
hardGoTo as navigate,
isSignedInSelector
} from '../../redux';
// eslint-disable-next-line max-len
import DonateServicebotEmbed from '../../components/Donation/components/DonateServicebotEmbed';
import { Loader, Spacer, Link } from '../../components/helpers';
const propTypes = {
donationEmails: PropTypes.array,
email: PropTypes.string,
isDonating: PropTypes.bool,
isSignedIn: PropTypes.bool,
navigate: PropTypes.func.isRequired,
showLoading: PropTypes.bool.isRequired
};
const mapStateToProps = createSelector(
isSignedInSelector,
userSelector,
signInLoadingSelector,
(isSignedIn, { email, isDonating, donationEmails }, showLoading) => ({
isSignedIn,
email,
isDonating,
donationEmails,
showLoading
})
);
const mapDispatchToProps = {
navigate
};
export class DonationSettingsPage extends Component {
constructor(...props) {
super(...props);
this.state = {
hash: null,
currentSettingsEmail: null
};
this.getEmailHmacHash = this.getEmailHmacHash.bind(this);
this.handleSelectDonationEmail = this.handleSelectDonationEmail.bind(this);
}
getEmailHmacHash(currentSettingsEmail) {
return postCreateHmacHash({
email: currentSettingsEmail
})
.then(response => {
const data = response && response.data;
this.setState({ hash: '' + data.hash, currentSettingsEmail });
})
.catch(error => {
const data =
error.response && error.response.data
? error.response.data
: {
error:
'Something is not right. Please contact team@freecodecamp.org'
};
console.error(data.error);
});
}
handleSelectDonationEmail(e) {
e.preventDefault();
this.setState({ hash: null, currentSettingsEmail: null });
this.getEmailHmacHash(e.currentTarget.value);
}
renderServicebotEmbed() {
const { currentSettingsEmail, hash } = this.state;
if (!hash && !currentSettingsEmail) {
return null;
}
return (
<div>
<Spacer />
<DonateServicebotEmbed email={currentSettingsEmail} hash={hash} />
</div>
);
}
renderDonationEmailsList() {
const { donationEmails } = this.props;
return (
<div>
{uniq(donationEmails).map((email, index) => (
<div key={email + '-' + index}>
<Button
bsStyle='primary'
className='btn btn-block'
onClick={this.handleSelectDonationEmail}
value={email}
>
{`Show donations for your ${email} email address`}
</Button>
<Spacer />
</div>
))}
</div>
);
}
render() {
const { showLoading, isSignedIn, isDonating, navigate } = this.props;
if (showLoading) {
return <Loader fullScreen={true} />;
}
if (!showLoading && !isSignedIn) {
navigate(`${apiLocation}/signin?returnTo=donation/settings`);
return <Loader fullScreen={true} />;
}
if (!showLoading && !isDonating) {
navigate(`/donate`);
return <Loader fullScreen={true} />;
}
return (
<Fragment>
<Helmet title='Manage your donation | freeCodeCamp.org' />
<Grid>
<Row>
<Col sm={6} smOffset={3} xs={12}>
<Spacer size={2} />
<Button block={true} bsStyle='primary' href='/donate'>
Go to donate page
</Button>
</Col>
</Row>
<Row>
<Col sm={8} smOffset={2} xs={12}>
<Spacer />
<h1 className='text-center'>Manage your donations</h1>
<Spacer />
<h3 className='text-center'>
Donations made using a credit or debit card
</h3>
</Col>
</Row>
<Row>
<Col sm={6} smOffset={3} xs={12}>
{this.renderDonationEmailsList()}
</Col>
</Row>
<Row>
<Col sm={8} smOffset={2} xs={12}>
{this.renderServicebotEmbed()}
</Col>
</Row>
<Row>
<Col sm={8} smOffset={2} xs={12}>
<hr />
<h3 className='text-center'>Donations made using PayPal</h3>
<p className='text-center'>
You can update your PayPal donation{' '}
<Link
external={true}
to='https://www.paypal.com/cgi-bin/webscr?cmd=_manage-paylist'
>
directly on PayPal
</Link>
.
</p>
</Col>
</Row>
<Row>
<Col sm={8} smOffset={2} xs={12}>
<hr />
<h3 className='text-center'>Still need help?</h3>
<p>
If you can't see your donation here, forward a donation receipt
you have recieved in your email to team@freeCodeCamp.org and
tell us how we can help you with it.
</p>
<Spacer />
</Col>
</Row>
</Grid>
</Fragment>
);
}
}
DonationSettingsPage.displayName = 'DonationSettingsPage';
DonationSettingsPage.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(DonationSettingsPage);

View File

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

View File

@@ -19,6 +19,14 @@ export const stripeScriptLoader = onload =>
onload
);
export const servicebotScriptLoader = () =>
scriptLoader(
'servicebot-billing-settings-embed.js',
'servicebot-billing-settings-embed.js',
true,
'https://js.servicebot.io/embeds/servicebot-billing-settings-embed.js'
);
export const mathJaxScriptLoader = () =>
scriptLoader(
'mathjax',