feat(donate): integrate servicebot
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import crypto from 'crypto';
|
||||||
import { isEmail, isNumeric } from 'validator';
|
import { isEmail, isNumeric } from 'validator';
|
||||||
|
|
||||||
import keys from '../../../config/secrets';
|
import keys from '../../../config/secrets';
|
||||||
@ -108,18 +109,12 @@ export default function donateBoot(app, done) {
|
|||||||
function createStripeDonation(req, res) {
|
function createStripeDonation(req, res) {
|
||||||
const { user, body } = req;
|
const { user, body } = req;
|
||||||
|
|
||||||
if (!user) {
|
if (!user || !body) {
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
.send({ error: 'User must be signed in for this request.' });
|
.send({ error: 'User must be signed in for this request.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body || !body.amount || !body.duration) {
|
|
||||||
return res.status(500).send({
|
|
||||||
error: 'The donation form had invalid values for this submission.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
amount,
|
amount,
|
||||||
duration,
|
duration,
|
||||||
@ -218,12 +213,54 @@ export default function donateBoot(app, done) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createHmacHash(req, res) {
|
||||||
|
const { user, body } = req;
|
||||||
|
|
||||||
|
if (!user || !body) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.send({ error: 'User must be signed in for this request.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email } = body;
|
||||||
|
|
||||||
|
if (!isEmail('' + email)) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.send({ error: 'The email is invalid for this request.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.donationEmails.includes(email)) {
|
||||||
|
return res.status(500).send({
|
||||||
|
error: `User does not have the email: ${email} associated with their donations.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`creating HMAC hash for ${email}`);
|
||||||
|
return Promise.resolve(email)
|
||||||
|
.then(email =>
|
||||||
|
crypto
|
||||||
|
.createHmac('sha256', keys.servicebot.hmacKey)
|
||||||
|
.update(email)
|
||||||
|
.digest('hex')
|
||||||
|
)
|
||||||
|
.then(hash => res.status(200).json({ hash }))
|
||||||
|
.catch(() =>
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.send({ error: 'Donation failed due to a server error.' })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const pubKey = keys.stripe.public;
|
const pubKey = keys.stripe.public;
|
||||||
const secKey = keys.stripe.secret;
|
const secKey = keys.stripe.secret;
|
||||||
const secretInvalid = !secKey || secKey === 'sk_from_stipe_dashboard';
|
const hmacKey = keys.servicebot.hmacKey;
|
||||||
const publicInvalid = !pubKey || pubKey === 'pk_from_stipe_dashboard';
|
const secretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard';
|
||||||
|
const publicInvalid = !pubKey || pubKey === 'pk_from_stripe_dashboard';
|
||||||
|
const hmacKeyInvalid =
|
||||||
|
!hmacKey || hmacKey === 'secret_key_from_servicebot_dashboard';
|
||||||
|
|
||||||
if (secretInvalid || publicInvalid) {
|
if (secretInvalid || publicInvalid || hmacKeyInvalid) {
|
||||||
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
||||||
throw new Error('Stripe API keys are required to boot the server!');
|
throw new Error('Stripe API keys are required to boot the server!');
|
||||||
}
|
}
|
||||||
@ -231,6 +268,7 @@ export default function donateBoot(app, done) {
|
|||||||
done();
|
done();
|
||||||
} else {
|
} else {
|
||||||
api.post('/charge-stripe', createStripeDonation);
|
api.post('/charge-stripe', createStripeDonation);
|
||||||
|
api.post('/create-hmac-hash', createHmacHash);
|
||||||
donateRouter.use('/donate', api);
|
donateRouter.use('/donate', api);
|
||||||
app.use(donateRouter);
|
app.use(donateRouter);
|
||||||
app.use('/internal', donateRouter);
|
app.use('/internal', donateRouter);
|
||||||
|
@ -51,7 +51,8 @@ export const userPropsForSession = [
|
|||||||
'completedProjectCount',
|
'completedProjectCount',
|
||||||
'completedCertCount',
|
'completedCertCount',
|
||||||
'completedLegacyCertCount',
|
'completedLegacyCertCount',
|
||||||
'acceptedPrivacyTerms'
|
'acceptedPrivacyTerms',
|
||||||
|
'donationEmails'
|
||||||
];
|
];
|
||||||
|
|
||||||
export function normaliseUserFields(user) {
|
export function normaliseUserFields(user) {
|
||||||
|
@ -42,7 +42,7 @@ function DonateCompletion({ processing, reset, success, error = null }) {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can update your supporter status at any time from the 'manage
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -30,6 +30,7 @@ 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 = {
|
||||||
|
enableDonationSettingsPage: PropTypes.func.isRequired,
|
||||||
isDonating: PropTypes.bool,
|
isDonating: PropTypes.bool,
|
||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
navigate: PropTypes.func.isRequired,
|
navigate: PropTypes.func.isRequired,
|
||||||
@ -204,7 +205,7 @@ class DonateForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderDonationOptions() {
|
renderDonationOptions() {
|
||||||
const { stripe } = this.props;
|
const { stripe, enableDonationSettingsPage } = this.props;
|
||||||
const {
|
const {
|
||||||
donationAmount,
|
donationAmount,
|
||||||
donationDuration,
|
donationDuration,
|
||||||
@ -242,6 +243,7 @@ class DonateForm extends Component {
|
|||||||
<DonateFormChildViewForHOC
|
<DonateFormChildViewForHOC
|
||||||
donationAmount={donationAmount}
|
donationAmount={donationAmount}
|
||||||
donationDuration={donationDuration}
|
donationDuration={donationDuration}
|
||||||
|
enableDonationSettingsPage={enableDonationSettingsPage}
|
||||||
getDonationButtonLabel={this.getDonationButtonLabel}
|
getDonationButtonLabel={this.getDonationButtonLabel}
|
||||||
hideAmountOptionsCB={this.hideAmountOptionsCB}
|
hideAmountOptionsCB={this.hideAmountOptionsCB}
|
||||||
/>
|
/>
|
||||||
@ -288,18 +290,6 @@ class DonateForm extends Component {
|
|||||||
this.renderDonationOptions()
|
this.renderDonationOptions()
|
||||||
)}
|
)}
|
||||||
</Col>
|
</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>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ const propTypes = {
|
|||||||
donationAmount: PropTypes.number.isRequired,
|
donationAmount: PropTypes.number.isRequired,
|
||||||
donationDuration: PropTypes.string.isRequired,
|
donationDuration: PropTypes.string.isRequired,
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
|
enableDonationSettingsPage: PropTypes.func.isRequired,
|
||||||
getDonationButtonLabel: PropTypes.func.isRequired,
|
getDonationButtonLabel: PropTypes.func.isRequired,
|
||||||
hideAmountOptionsCB: PropTypes.func.isRequired,
|
hideAmountOptionsCB: PropTypes.func.isRequired,
|
||||||
isSignedIn: PropTypes.bool,
|
isSignedIn: PropTypes.bool,
|
||||||
@ -119,6 +120,7 @@ class DonateFormChildViewForHOC extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
postDonation(token) {
|
postDonation(token) {
|
||||||
|
const { enableDonationSettingsPage } = this.props;
|
||||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
const { donationAmount: amount, donationDuration: duration } = this.state;
|
||||||
this.setState(state => ({
|
this.setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
@ -144,6 +146,7 @@ class DonateFormChildViewForHOC extends Component {
|
|||||||
error: data.error ? data.error : null
|
error: data.error ? data.error : null
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
enableDonationSettingsPage();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
const data =
|
const data =
|
||||||
|
@ -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;
|
@ -3,32 +3,40 @@ import Helmet from 'react-helmet';
|
|||||||
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 { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
import { Grid, Row, Col, Button } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
import { stripePublicKey } from '../../config/env.json';
|
import { stripePublicKey } from '../../config/env.json';
|
||||||
import { Spacer, Loader } from '../components/helpers';
|
import { Spacer, Loader } from '../components/helpers';
|
||||||
import DonateForm from '../components/Donation/components/DonateForm';
|
import DonateForm from '../components/Donation/components/DonateForm';
|
||||||
import DonateText from '../components/Donation/components/DonateText';
|
import DonateText from '../components/Donation/components/DonateText';
|
||||||
import { signInLoadingSelector } from '../redux';
|
import { signInLoadingSelector, userSelector } from '../redux';
|
||||||
import { stripeScriptLoader } from '../utils/scriptLoaders';
|
import { stripeScriptLoader } from '../utils/scriptLoaders';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
isDonating: PropTypes.bool,
|
||||||
|
showLoading: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
showLoading => ({
|
({ isDonating }, showLoading) => ({
|
||||||
|
isDonating,
|
||||||
showLoading
|
showLoading
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
showLoading: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DonatePage extends Component {
|
export class DonatePage extends Component {
|
||||||
constructor(...props) {
|
constructor(...props) {
|
||||||
super(...props);
|
super(...props);
|
||||||
this.state = {
|
this.state = {
|
||||||
stripe: null
|
stripe: null,
|
||||||
|
enableSettings: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.enableDonationSettingsPage = this.enableDonationSettingsPage.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
this.handleStripeLoad = this.handleStripeLoad.bind(this);
|
this.handleStripeLoad = this.handleStripeLoad.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,9 +68,14 @@ export class DonatePage extends Component {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableDonationSettingsPage(enableSettings = true) {
|
||||||
|
this.setState({ enableSettings });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { stripe } = this.state;
|
const { stripe } = this.state;
|
||||||
const { showLoading } = this.props;
|
const { showLoading, isDonating } = this.props;
|
||||||
|
const { enableSettings } = this.state;
|
||||||
|
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
return <Loader fullScreen={true} />;
|
return <Loader fullScreen={true} />;
|
||||||
@ -81,7 +94,32 @@ export class DonatePage extends Component {
|
|||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col md={6}>
|
<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>
|
||||||
<Col md={6}>
|
<Col md={6}>
|
||||||
<DonateText />
|
<DonateText />
|
||||||
|
209
client/src/pages/donation/settings.js
Normal file
209
client/src/pages/donation/settings.js
Normal 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);
|
@ -54,6 +54,10 @@ export function postChargeStripe(body) {
|
|||||||
return post(`/donate/charge-stripe`, body);
|
return post(`/donate/charge-stripe`, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function postCreateHmacHash(body) {
|
||||||
|
return post(`/donate/create-hmac-hash`, body);
|
||||||
|
}
|
||||||
|
|
||||||
export function putUpdateLegacyCert(body) {
|
export function putUpdateLegacyCert(body) {
|
||||||
return post('/update-my-projects', body);
|
return post('/update-my-projects', body);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,14 @@ export const stripeScriptLoader = onload =>
|
|||||||
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 = () =>
|
export const mathJaxScriptLoader = () =>
|
||||||
scriptLoader(
|
scriptLoader(
|
||||||
'mathjax',
|
'mathjax',
|
||||||
|
@ -13,7 +13,8 @@ const {
|
|||||||
FORUM_PROXY: forumProxy,
|
FORUM_PROXY: forumProxy,
|
||||||
NEWS_PROXY: newsProxy,
|
NEWS_PROXY: newsProxy,
|
||||||
LOCALE: locale,
|
LOCALE: locale,
|
||||||
STRIPE_PUBLIC: stripePublicKey,
|
STRIPE_PUBLIC_KEY: stripePublicKey,
|
||||||
|
SERVICEBOT_ID: servicebotId,
|
||||||
ALGOLIA_APP_ID: algoliaAppId,
|
ALGOLIA_APP_ID: algoliaAppId,
|
||||||
ALGOLIA_API_KEY: algoliaAPIKey
|
ALGOLIA_API_KEY: algoliaAPIKey
|
||||||
} = process.env;
|
} = process.env;
|
||||||
@ -30,6 +31,7 @@ const locations = {
|
|||||||
module.exports = Object.assign(locations, {
|
module.exports = Object.assign(locations, {
|
||||||
locale,
|
locale,
|
||||||
stripePublicKey,
|
stripePublicKey,
|
||||||
|
servicebotId,
|
||||||
algoliaAppId:
|
algoliaAppId:
|
||||||
!algoliaAppId || algoliaAppId === 'Algolia app id from dashboard'
|
!algoliaAppId || algoliaAppId === 'Algolia app id from dashboard'
|
||||||
? null
|
? null
|
||||||
|
@ -30,8 +30,10 @@ const {
|
|||||||
ROLLBAR_APP_ID,
|
ROLLBAR_APP_ID,
|
||||||
ROLLBAR_CLIENT_ID,
|
ROLLBAR_CLIENT_ID,
|
||||||
|
|
||||||
STRIPE_PUBLIC,
|
STRIPE_PUBLIC_KEY,
|
||||||
STRIPE_SECRET
|
STRIPE_SECRET_KEY,
|
||||||
|
SERVICEBOT_ID,
|
||||||
|
SERVICEBOT_HMAC_SECRET_KEY
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -92,7 +94,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
stripe: {
|
stripe: {
|
||||||
public: STRIPE_PUBLIC,
|
public: STRIPE_PUBLIC_KEY,
|
||||||
secret: STRIPE_SECRET
|
secret: STRIPE_SECRET_KEY
|
||||||
|
},
|
||||||
|
|
||||||
|
servicebot: {
|
||||||
|
servicebotId: SERVICEBOT_ID,
|
||||||
|
hmacKey: SERVICEBOT_HMAC_SECRET_KEY
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user