feat(donate): updated donate page and plans
- [x] two column layout for the page. - [x] amount to learning minutes mapping for contribution impact. - [x] handle one-time and recurring stripe subscription charges. - [x] server side validation of donate forms. - [x] prevent multiple subscriptions and onetime donations per user.
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import Stripe from 'stripe';
|
||||
import debug from 'debug';
|
||||
import { isEmail, isNumeric } from 'validator';
|
||||
|
||||
import keys from '../../../config/secrets';
|
||||
|
||||
@ -10,41 +11,79 @@ export default function donateBoot(app, done) {
|
||||
const { User } = app.models;
|
||||
const api = app.loopback.Router();
|
||||
const donateRouter = app.loopback.Router();
|
||||
const subscriptionPlans = [500, 1000, 3500, 5000, 25000].reduce(
|
||||
(accu, current) => ({
|
||||
...accu,
|
||||
[current]: {
|
||||
amount: current,
|
||||
interval: 'month',
|
||||
product: {
|
||||
name:
|
||||
'Monthly Donation to freeCodeCamp.org - ' +
|
||||
`Thank you ($${current / 100})`
|
||||
},
|
||||
currency: 'usd',
|
||||
id: `monthly-donation-${current}`
|
||||
}
|
||||
}),
|
||||
{}
|
||||
|
||||
const durationKeys = ['year', 'month', 'onetime'];
|
||||
const donationOneTimeConfig = [100000, 25000, 3500];
|
||||
const donationSubscriptionConfig = {
|
||||
duration: {
|
||||
year: 'Yearly',
|
||||
month: 'Monthly'
|
||||
},
|
||||
plans: {
|
||||
year: [100000, 25000, 3500],
|
||||
month: [5000, 3500, 500]
|
||||
}
|
||||
};
|
||||
|
||||
const subscriptionPlans = Object.keys(
|
||||
donationSubscriptionConfig.plans
|
||||
).reduce(
|
||||
(prevDuration, duration) =>
|
||||
prevDuration.concat(
|
||||
donationSubscriptionConfig.plans[duration].reduce(
|
||||
(prevAmount, amount) =>
|
||||
prevAmount.concat({
|
||||
amount: amount,
|
||||
interval: duration,
|
||||
product: {
|
||||
name: `${
|
||||
donationSubscriptionConfig.duration[duration]
|
||||
} Donation to freeCodeCamp.org - Thank you ($${amount / 100})`,
|
||||
metadata: {
|
||||
/* eslint-disable camelcase */
|
||||
sb_service: `freeCodeCamp.org`,
|
||||
sb_tier: `${
|
||||
donationSubscriptionConfig.duration[duration]
|
||||
} $${amount / 100} Donation`
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
},
|
||||
currency: 'usd',
|
||||
id: `${donationSubscriptionConfig.duration[
|
||||
duration
|
||||
].toLowerCase()}-donation-${amount}`
|
||||
}),
|
||||
[]
|
||||
)
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
function validStripeForm(amount, duration, email) {
|
||||
return isEmail('' + email) &&
|
||||
isNumeric('' + amount) &&
|
||||
durationKeys.includes(duration) &&
|
||||
duration === 'onetime'
|
||||
? donationOneTimeConfig.includes(amount)
|
||||
: donationSubscriptionConfig.plans[duration];
|
||||
}
|
||||
|
||||
function connectToStripe() {
|
||||
return new Promise(function(resolve) {
|
||||
// connect to stripe API
|
||||
stripe = Stripe(keys.stripe.secret);
|
||||
// parse stripe plans
|
||||
stripe.plans.list({}, function(err, plans) {
|
||||
stripe.plans.list({}, function(err, stripePlans) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
const requiredPlans = Object.keys(subscriptionPlans).map(
|
||||
key => subscriptionPlans[key].id
|
||||
);
|
||||
const availablePlans = plans.data.map(plan => plan.id);
|
||||
requiredPlans.forEach(planId => {
|
||||
if (!availablePlans.includes(planId)) {
|
||||
const key = planId.split('-').slice(-1)[0];
|
||||
createStripePlan(subscriptionPlans[key]);
|
||||
const requiredPlans = subscriptionPlans.map(plan => plan.id);
|
||||
const availablePlans = stripePlans.data.map(plan => plan.id);
|
||||
requiredPlans.forEach(requiredPlan => {
|
||||
if (!availablePlans.includes(requiredPlan)) {
|
||||
createStripePlan(
|
||||
subscriptionPlans.find(plan => plan.id === requiredPlan)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -53,12 +92,12 @@ export default function donateBoot(app, done) {
|
||||
}
|
||||
|
||||
function createStripePlan(plan) {
|
||||
log(`Creating subscription plan: ${plan.product.name}`);
|
||||
stripe.plans.create(plan, function(err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
log(err);
|
||||
}
|
||||
console.log(`${plan.id} created`);
|
||||
log(`Created plan with plan id: ${plan.id}`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
@ -66,15 +105,24 @@ export default function donateBoot(app, done) {
|
||||
function createStripeDonation(req, res) {
|
||||
const { user, body } = req;
|
||||
|
||||
if (!body || !body.amount) {
|
||||
return res.status(400).send({ error: 'Amount Required' });
|
||||
if (!body || !body.amount || !body.duration) {
|
||||
return res.status(400).send({ error: 'Amount and duration Required.' });
|
||||
}
|
||||
|
||||
const {
|
||||
amount,
|
||||
duration,
|
||||
token: { email, id }
|
||||
} = body;
|
||||
|
||||
if (!validStripeForm(amount, duration, email)) {
|
||||
return res
|
||||
.status(500)
|
||||
.send({ error: 'Invalid donation form values submitted' });
|
||||
}
|
||||
|
||||
const isOneTime = duration === 'onetime' ? true : false;
|
||||
|
||||
const fccUser = user
|
||||
? Promise.resolve(user)
|
||||
: new Promise((resolve, reject) =>
|
||||
@ -95,46 +143,85 @@ export default function donateBoot(app, done) {
|
||||
let donation = {
|
||||
email,
|
||||
amount,
|
||||
duration,
|
||||
provider: 'stripe',
|
||||
startDate: new Date(Date.now()).toISOString()
|
||||
};
|
||||
|
||||
const createCustomer = user => {
|
||||
donatingUser = user;
|
||||
return stripe.customers.create({
|
||||
email,
|
||||
card: id
|
||||
});
|
||||
};
|
||||
|
||||
const createSubscription = customer => {
|
||||
donation.customerId = customer.id;
|
||||
return stripe.subscriptions.create({
|
||||
customer: customer.id,
|
||||
items: [
|
||||
{
|
||||
plan: `${donationSubscriptionConfig.duration[
|
||||
duration
|
||||
].toLowerCase()}-donation-${amount}`
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const createOneTimeCharge = customer => {
|
||||
donation.customerId = customer.id;
|
||||
return stripe.charges.create({
|
||||
amount: amount,
|
||||
currency: 'usd',
|
||||
customer: customer.id
|
||||
});
|
||||
};
|
||||
|
||||
const createAsyncUserDonation = () => {
|
||||
donatingUser
|
||||
.createDonation(donation)
|
||||
.toPromise()
|
||||
.catch(err => {
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
|
||||
return fccUser
|
||||
.then(user => {
|
||||
donatingUser = user;
|
||||
return stripe.customers.create({
|
||||
email,
|
||||
card: id
|
||||
});
|
||||
const { isDonating } = user;
|
||||
if (isDonating) {
|
||||
throw {
|
||||
message: `User already has active donation(s).`,
|
||||
type: 'AlreadyDonatingError'
|
||||
};
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.then(createCustomer)
|
||||
.then(customer => {
|
||||
donation.customerId = customer.id;
|
||||
return stripe.subscriptions.create({
|
||||
customer: customer.id,
|
||||
items: [
|
||||
{
|
||||
plan: `monthly-donation-${amount}`
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
.then(subscription => {
|
||||
donation.subscriptionId = subscription.id;
|
||||
return res.send(subscription);
|
||||
})
|
||||
.then(() => {
|
||||
donatingUser
|
||||
.createDonation(donation)
|
||||
.toPromise()
|
||||
.catch(err => {
|
||||
throw new Error(err);
|
||||
});
|
||||
return isOneTime
|
||||
? createOneTimeCharge(customer).then(charge => {
|
||||
donation.subscriptionId = 'one-time-charge-prefix-' + charge.id;
|
||||
return res.send(charge);
|
||||
})
|
||||
: createSubscription(customer).then(subscription => {
|
||||
donation.subscriptionId = subscription.id;
|
||||
return res.send(subscription);
|
||||
});
|
||||
})
|
||||
.then(createAsyncUserDonation)
|
||||
.catch(err => {
|
||||
if (err.type === 'StripeCardError') {
|
||||
if (
|
||||
err.type === 'StripeCardError' ||
|
||||
err.type === 'AlreadyDonatingError'
|
||||
) {
|
||||
return res.status(402).send({ error: err.message });
|
||||
}
|
||||
return res.status(500).send({ error: 'Donation Failed' });
|
||||
return res
|
||||
.status(500)
|
||||
.send({ error: 'Donation failed due to a server error.' });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,9 @@
|
||||
"required": true,
|
||||
"description": "The donation amount in cents"
|
||||
},
|
||||
"duration": {
|
||||
"type": "string"
|
||||
},
|
||||
"startDate": {
|
||||
"type": "DateString",
|
||||
"required": true
|
||||
|
@ -1,21 +1,3 @@
|
||||
.donation-modal {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.donation-modal p {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.donation-modal .alert {
|
||||
width: 60%;
|
||||
margin: 0 auto 0;
|
||||
}
|
||||
|
||||
.donation-modal .modal-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.donation-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -63,21 +45,6 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.modal-close-btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-close-btn-container a {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.modal-close-btn-container a:hover {
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.donate-input-element {
|
||||
padding-top: 8px;
|
||||
}
|
||||
@ -97,6 +64,92 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.donate-other .btn-cta {
|
||||
margin: 7px 0px;
|
||||
.donate-tabs > .nav-pills {
|
||||
display: flex;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
.donate-tabs > .nav-pills > li {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.donate-tabs > .nav-pills > li > a {
|
||||
text-transform: capitalize;
|
||||
text-decoration: none;
|
||||
border: 2px solid var(--yellow-light);
|
||||
border-radius: 0px;
|
||||
color: var(--gray-85);
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.donate-tabs > .nav-pills > li > a:hover,
|
||||
.donate-tabs > .nav-pills > li > a:focus {
|
||||
background-color: #ffe18f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.donate-tabs > .nav-pills > li.active > a,
|
||||
.donate-tabs > .nav-pills > li.active > a:hover,
|
||||
.donate-tabs > .nav-pills > li.active > a:focus {
|
||||
color: var(--gray-85);
|
||||
background-color: var(--yellow-light);
|
||||
border: 2px solid var(--yellow-light);
|
||||
text-decoration: none;
|
||||
border-radius: 0px;
|
||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
.amount-values {
|
||||
display: flex;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.amount-values > label {
|
||||
margin: 0 2px !important;
|
||||
color: var(--gray-85);
|
||||
border: 2px solid var(--yellow-light);
|
||||
border-radius: 0px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.amount-values > label:hover,
|
||||
.amount-values > label:focus,
|
||||
.amount-values > label:active:hover {
|
||||
background-color: #ffe18f;
|
||||
cursor: pointer;
|
||||
color: var(--gray-85);
|
||||
border-color: var(--yellow-light);
|
||||
}
|
||||
|
||||
.amount-values > label:focus,
|
||||
.amount-values > label:active:hover {
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
outline-color: -webkit-focus-ring-color;
|
||||
outline-style: auto;
|
||||
outline-width: 5px;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.amount-values > label.active,
|
||||
.amount-values > label.active:hover,
|
||||
.amount-values > label.active:focus {
|
||||
color: var(--gray-85);
|
||||
background-color: var(--yellow-light);
|
||||
border: 2px solid var(--yellow-light);
|
||||
border-radius: 0px;
|
||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
|
||||
}
|
||||
|
||||
li.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
li.disabled > a {
|
||||
border: 2px solid var(--gray-15) !important;
|
||||
color: var(--gray-15) !important;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ function DonateCompletion({ processing, reset, success, error = null }) {
|
||||
const heading = processing
|
||||
? 'We are processing your donation.'
|
||||
: success
|
||||
? 'Your donation was successful.'
|
||||
? 'Thank you for being a supporter.'
|
||||
: 'Something went wrong with your donation.';
|
||||
return (
|
||||
<Alert bsStyle={style} className='donation-completion'>
|
||||
@ -37,12 +37,12 @@ function DonateCompletion({ processing, reset, success, error = null }) {
|
||||
{success && (
|
||||
<div>
|
||||
<p>
|
||||
You can update your supporter status at any time from your account
|
||||
settings.
|
||||
Your donation will support free technology education for people
|
||||
all over the world.
|
||||
</p>
|
||||
<p>
|
||||
Thank you for supporting free technology education for people all
|
||||
over the world.
|
||||
You can update your supporter status at any time from the 'manage
|
||||
your existing donation' section on this page.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
@ -2,9 +2,19 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button } from '@freecodecamp/react-bootstrap';
|
||||
import {
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
Row,
|
||||
Col,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Radio
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import { StripeProvider, Elements } from 'react-stripe-elements';
|
||||
import { apiLocation } from '../../../../config/env.json';
|
||||
import Spacer from '../../helpers/Spacer';
|
||||
import DonateFormChildViewForHOC from './DonateFormChildViewForHOC';
|
||||
import {
|
||||
userSelector,
|
||||
@ -14,8 +24,13 @@ import {
|
||||
} from '../../../redux';
|
||||
|
||||
import '../Donation.css';
|
||||
import DonateCompletion from './DonateCompletion.js';
|
||||
|
||||
const numToCommas = num =>
|
||||
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
|
||||
const propTypes = {
|
||||
isDonating: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
showLoading: PropTypes.bool.isRequired,
|
||||
@ -28,11 +43,10 @@ const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
signInLoadingSelector,
|
||||
isSignedInSelector,
|
||||
({ email, theme }, showLoading, isSignedIn) => ({
|
||||
email,
|
||||
theme,
|
||||
showLoading,
|
||||
isSignedIn
|
||||
({ isDonating }, showLoading, isSignedIn) => ({
|
||||
isDonating,
|
||||
isSignedIn,
|
||||
showLoading
|
||||
})
|
||||
);
|
||||
|
||||
@ -44,66 +58,246 @@ const createOnClick = navigate => e => {
|
||||
e.preventDefault();
|
||||
return navigate(`${apiLocation}/signin?returnTo=donate`);
|
||||
};
|
||||
|
||||
class DonateForm extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
donationAmount: 500
|
||||
processing: false,
|
||||
isDonating: this.props.isDonating,
|
||||
donationAmount: 5000,
|
||||
donationDuration: 'month',
|
||||
paymentType: 'Card'
|
||||
};
|
||||
|
||||
this.buttonSingleAmounts = [2500, 5000, 10000, 25000];
|
||||
this.buttonMonthlyAmounts = [500, 1000, 2000];
|
||||
this.buttonAnnualAmounts = [6000, 10000, 25000, 50000];
|
||||
this.durations = {
|
||||
year: 'yearly',
|
||||
month: 'monthly',
|
||||
onetime: 'one-time'
|
||||
};
|
||||
this.amounts = {
|
||||
year: [100000, 25000, 3500],
|
||||
month: [5000, 3500, 500],
|
||||
onetime: [100000, 25000, 3500]
|
||||
};
|
||||
|
||||
this.isActive = this.isActive.bind(this);
|
||||
this.renderAmountButtons = this.renderAmountButtons.bind(this);
|
||||
this.getActiveDonationAmount = this.getActiveDonationAmount.bind(this);
|
||||
this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this);
|
||||
this.handleSelectAmount = this.handleSelectAmount.bind(this);
|
||||
this.handleSelectDuration = this.handleSelectDuration.bind(this);
|
||||
this.handleSelectPaymentType = this.handleSelectPaymentType.bind(this);
|
||||
this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this);
|
||||
}
|
||||
|
||||
isActive(amount) {
|
||||
return this.state.donationAmount === amount;
|
||||
getActiveDonationAmount(durationSelected, amountSelected) {
|
||||
return this.amounts[durationSelected].includes(amountSelected)
|
||||
? amountSelected
|
||||
: this.amounts[durationSelected][0];
|
||||
}
|
||||
|
||||
renderAmountButtons() {
|
||||
return this.buttonAnnualAmounts.map(amount => (
|
||||
<Button
|
||||
className={`amount-value ${this.isActive(amount) ? 'active' : ''}`}
|
||||
href=''
|
||||
id={amount}
|
||||
key={'amount-' + amount}
|
||||
onClick={this.handleAmountClick}
|
||||
tabIndex='-1'
|
||||
convertToTimeContributed(amount) {
|
||||
return `${numToCommas((amount / 100) * 50 * 60)} minutes`;
|
||||
}
|
||||
|
||||
getFormatedAmountLabel(amount) {
|
||||
return `$${numToCommas(amount / 100)}`;
|
||||
}
|
||||
|
||||
getDonationButtonLabel() {
|
||||
const { donationAmount, donationDuration } = this.state;
|
||||
let donationBtnLabel = `Confirm your donation`;
|
||||
if (donationDuration === 'onetime') {
|
||||
donationBtnLabel = `Confirm your one-time donation of ${this.getFormatedAmountLabel(
|
||||
donationAmount
|
||||
)}`;
|
||||
} else {
|
||||
donationBtnLabel = `Confirm your donation of ${this.getFormatedAmountLabel(
|
||||
donationAmount
|
||||
)} ${donationDuration === 'month' ? 'per month' : 'per year'}`;
|
||||
}
|
||||
return donationBtnLabel;
|
||||
}
|
||||
|
||||
handleSelectDuration(donationDuration) {
|
||||
const donationAmount = this.getActiveDonationAmount(donationDuration, 0);
|
||||
this.setState({ donationDuration, donationAmount });
|
||||
}
|
||||
|
||||
handleSelectAmount(donationAmount) {
|
||||
this.setState({ donationAmount });
|
||||
}
|
||||
|
||||
handleSelectPaymentType(e) {
|
||||
this.setState({
|
||||
paymentType: e.currentTarget.value
|
||||
});
|
||||
}
|
||||
|
||||
renderAmountButtons(duration) {
|
||||
return this.amounts[duration].map(amount => (
|
||||
<ToggleButton
|
||||
className='amount-value'
|
||||
id={`${this.durations[duration]}-donation-${amount}`}
|
||||
key={`${this.durations[duration]}-donation-${amount}`}
|
||||
value={amount}
|
||||
>
|
||||
{`$${amount / 100}`}
|
||||
</Button>
|
||||
{this.getFormatedAmountLabel(amount)}
|
||||
</ToggleButton>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSignedIn, navigate, showLoading, stripe } = this.props;
|
||||
renderDurationAmountOptions() {
|
||||
const { donationAmount, donationDuration, processing } = this.state;
|
||||
return !processing ? (
|
||||
<div>
|
||||
<h3>Duration and amount:</h3>
|
||||
<Tabs
|
||||
activeKey={donationDuration}
|
||||
animation={false}
|
||||
bsStyle='pills'
|
||||
className='donate-tabs'
|
||||
id='Duration'
|
||||
onSelect={this.handleSelectDuration}
|
||||
>
|
||||
{Object.keys(this.durations).map(duration => (
|
||||
<Tab
|
||||
eventKey={duration}
|
||||
key={duration}
|
||||
title={this.durations[duration]}
|
||||
>
|
||||
<Spacer />
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
animation={`false`}
|
||||
className='amount-values'
|
||||
name='amounts'
|
||||
onChange={this.handleSelectAmount}
|
||||
type='radio'
|
||||
value={this.getActiveDonationAmount(duration, donationAmount)}
|
||||
>
|
||||
{this.renderAmountButtons(duration)}
|
||||
</ToggleButtonGroup>
|
||||
<Spacer />
|
||||
<p>
|
||||
{`Your `}
|
||||
{this.getFormatedAmountLabel(donationAmount)}
|
||||
{` donation will provide `}
|
||||
{this.convertToTimeContributed(donationAmount)}
|
||||
{` of learning to people around the world `}
|
||||
{duration === 'one-time' ? `for one ` : `each `}
|
||||
{duration === 'monthly' ? `month.` : `year.`}
|
||||
</p>
|
||||
</div>
|
||||
</Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
if (!showLoading && !isSignedIn) {
|
||||
hideAmountOptionsCB(hide) {
|
||||
this.setState({ processing: hide });
|
||||
}
|
||||
|
||||
renderDonationOptions() {
|
||||
const { stripe } = this.props;
|
||||
const {
|
||||
donationAmount,
|
||||
donationDuration,
|
||||
paymentType,
|
||||
processing
|
||||
} = this.state;
|
||||
return (
|
||||
<div>
|
||||
{!processing ? (
|
||||
<div>
|
||||
<Radio
|
||||
checked={paymentType === 'Card'}
|
||||
name='payment-method'
|
||||
onChange={this.handleSelectPaymentType}
|
||||
value='Card'
|
||||
>
|
||||
Donate using a Credit/Debit Card.
|
||||
</Radio>
|
||||
<Radio
|
||||
checked={paymentType === 'PayPal'}
|
||||
// disable the paypal integration for now
|
||||
disabled={true}
|
||||
name='payment-method'
|
||||
onChange={this.handleSelectPaymentType}
|
||||
value='PayPal'
|
||||
>
|
||||
Donate using PayPal. (Coming soon)
|
||||
</Radio>
|
||||
<Spacer />
|
||||
</div>
|
||||
) : null}
|
||||
{paymentType === 'Card' ? (
|
||||
<StripeProvider stripe={stripe}>
|
||||
<Elements>
|
||||
<DonateFormChildViewForHOC
|
||||
donationAmount={donationAmount}
|
||||
donationDuration={donationDuration}
|
||||
getDonationButtonLabel={this.getDonationButtonLabel}
|
||||
hideAmountOptionsCB={this.hideAmountOptionsCB}
|
||||
/>
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
) : (
|
||||
<p>
|
||||
PayPal is currently unavailable. Please use a Credit/Debit card
|
||||
instead.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSignedIn, navigate, showLoading, isDonating } = this.props;
|
||||
|
||||
if (isDonating) {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
bsStyle='default'
|
||||
className='btn btn-block'
|
||||
onClick={createOnClick(navigate)}
|
||||
>
|
||||
Become a supporter
|
||||
</Button>
|
||||
</div>
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<DonateCompletion success={true} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StripeProvider stripe={stripe}>
|
||||
<Elements>
|
||||
<DonateFormChildViewForHOC />
|
||||
</Elements>
|
||||
</StripeProvider>
|
||||
</div>
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
{this.renderDurationAmountOptions()}
|
||||
</Col>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
{!showLoading && !isSignedIn ? (
|
||||
<Button
|
||||
bsStyle='default'
|
||||
className='btn btn-block'
|
||||
onClick={createOnClick(navigate)}
|
||||
>
|
||||
Become a supporter
|
||||
</Button>
|
||||
) : (
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,14 +12,17 @@ import {
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import { injectStripe } from 'react-stripe-elements';
|
||||
|
||||
import Spacer from '../../helpers/Spacer';
|
||||
import StripeCardForm from './StripeCardForm';
|
||||
import DonateCompletion from './DonateCompletion';
|
||||
import { postChargeStripe } from '../../../utils/ajax';
|
||||
import { userSelector, isSignedInSelector } from '../../../redux';
|
||||
|
||||
const propTypes = {
|
||||
donationAmount: PropTypes.number.isRequired,
|
||||
donationDuration: PropTypes.string.isRequired,
|
||||
email: PropTypes.string,
|
||||
getDonationButtonLabel: PropTypes.func.isRequired,
|
||||
hideAmountOptionsCB: PropTypes.func.isRequired,
|
||||
isSignedIn: PropTypes.bool,
|
||||
stripe: PropTypes.shape({
|
||||
createToken: PropTypes.func.isRequired
|
||||
@ -27,7 +30,6 @@ const propTypes = {
|
||||
theme: PropTypes.string
|
||||
};
|
||||
const initialState = {
|
||||
donationAmount: 500,
|
||||
donationState: {
|
||||
processing: false,
|
||||
success: false,
|
||||
@ -47,6 +49,8 @@ class DonateFormChildViewForHOC extends Component {
|
||||
|
||||
this.state = {
|
||||
...initialState,
|
||||
donationAmount: this.props.donationAmount,
|
||||
donationDuration: this.props.donationDuration,
|
||||
email: null,
|
||||
isFormValid: false
|
||||
};
|
||||
@ -82,6 +86,7 @@ class DonateFormChildViewForHOC extends Component {
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.hideAmountOptions(true);
|
||||
const email = this.getUserEmail();
|
||||
if (!email || !isEmail(email)) {
|
||||
return this.setState(state => ({
|
||||
@ -110,8 +115,13 @@ class DonateFormChildViewForHOC extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
hideAmountOptions(hide) {
|
||||
const { hideAmountOptionsCB } = this.props;
|
||||
hideAmountOptionsCB(hide);
|
||||
}
|
||||
|
||||
postDonation(token) {
|
||||
const { donationAmount: amount } = this.state;
|
||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
||||
const { isSignedIn } = this.props;
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
@ -123,7 +133,8 @@ class DonateFormChildViewForHOC extends Component {
|
||||
|
||||
return postChargeStripe(isSignedIn, {
|
||||
token,
|
||||
amount
|
||||
amount,
|
||||
duration
|
||||
})
|
||||
.then(response => {
|
||||
const data = response && response.data;
|
||||
@ -167,7 +178,7 @@ class DonateFormChildViewForHOC extends Component {
|
||||
|
||||
renderDonateForm() {
|
||||
const { isFormValid } = this.state;
|
||||
const { theme } = this.props;
|
||||
const { theme, getDonationButtonLabel } = this.props;
|
||||
return (
|
||||
<Form className='donation-form' onSubmit={this.handleSubmit}>
|
||||
<FormGroup className='donation-email-container'>
|
||||
@ -193,13 +204,16 @@ class DonateFormChildViewForHOC extends Component {
|
||||
id='confirm-donation-btn'
|
||||
type='submit'
|
||||
>
|
||||
Confirm your donation of $5 / month
|
||||
{getDonationButtonLabel()}
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ donationAmount, donationDuration }) {
|
||||
this.setState({ donationAmount, donationDuration });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
donationState: { processing, success, error }
|
||||
@ -212,6 +226,7 @@ class DonateFormChildViewForHOC extends Component {
|
||||
reset: this.resetDonation
|
||||
});
|
||||
}
|
||||
this.hideAmountOptions(false);
|
||||
return this.renderDonateForm();
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Row, Col } from '@freecodecamp/react-bootstrap';
|
||||
|
||||
const DonateText = () => {
|
||||
return (
|
||||
<>
|
||||
<p>freeCodeCamp is a highly efficient education nonprofit.</p>
|
||||
<p>
|
||||
In 2019 alone, we provided 1,100,000,000 minutes of free education to
|
||||
people around the world.
|
||||
</p>
|
||||
<p>
|
||||
Since freeCodeCamp's total budget is only $373,000, that means every
|
||||
dollar you donate to freeCodeCamp translates into 50 hours worth of
|
||||
technology education.
|
||||
</p>
|
||||
<p>
|
||||
When you donate to freeCodeCamp, you help people learn new skills and
|
||||
provide for their families.
|
||||
</p>
|
||||
<p>
|
||||
You also help us create new resources for you to use to expand your own
|
||||
technology skills.
|
||||
</p>
|
||||
</>
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<p>freeCodeCamp is a highly efficient education nonprofit.</p>
|
||||
<p>
|
||||
In 2019 alone, we provided 1,100,000,000 minutes of free education to
|
||||
people around the world.
|
||||
</p>
|
||||
<p>
|
||||
Since freeCodeCamp's total budget is only $373,000, that means every
|
||||
dollar you donate to freeCodeCamp translates into 50 hours worth of
|
||||
technology education.
|
||||
</p>
|
||||
<p>
|
||||
When you donate to freeCodeCamp, you help people learn new skills and
|
||||
provide for their families.
|
||||
</p>
|
||||
<p>
|
||||
You also help us create new resources for you to use to expand your
|
||||
own technology skills.
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -72,21 +72,19 @@ export class DonatePage extends Component {
|
||||
<Fragment>
|
||||
<Helmet title='Support our nonprofit | freeCodeCamp.org' />
|
||||
<Grid>
|
||||
<Spacer />
|
||||
<Row>
|
||||
<Col sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<h2 className='text-center'>Become a Supporter</h2>
|
||||
<h1 className='text-center'>Become a Supporter</h1>
|
||||
<Spacer />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col sm={8} smOffset={2} xs={12}>
|
||||
<DonateText />
|
||||
<Spacer />
|
||||
</Col>
|
||||
<Col sm={6} smOffset={3} xs={12}>
|
||||
<Col md={6}>
|
||||
<DonateForm stripe={stripe} />
|
||||
<Spacer />
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<DonateText />
|
||||
</Col>
|
||||
</Row>
|
||||
</Grid>
|
||||
|
Reference in New Issue
Block a user