diff --git a/api-server/src/server/boot/donate.js b/api-server/src/server/boot/donate.js
index 21e5aa4f29..abf52df6ef 100644
--- a/api-server/src/server/boot/donate.js
+++ b/api-server/src/server/boot/donate.js
@@ -11,9 +11,13 @@ import {
import {
durationKeysConfig,
donationOneTimeConfig,
- donationSubscriptionConfig
+ donationSubscriptionConfig,
+ durationsConfig,
+ onetimeSKUConfig,
+ donationUrls
} from '../../../../config/donation-settings';
import keys from '../../../../config/secrets';
+import { deploymentEnv } from '../../../../config/env';
const log = debug('fcc:boot:donate');
@@ -246,6 +250,53 @@ export default function donateBoot(app, done) {
});
}
+ async function createStripeSession(req, res) {
+ const {
+ body,
+ body: { donationAmount, donationDuration }
+ } = req;
+ if (!body) {
+ return res
+ .status(500)
+ .send({ type: 'danger', message: 'Request has not completed.' });
+ }
+ const isSubscription = donationDuration !== 'onetime';
+ const getSKUId = () => {
+ const { id } = onetimeSKUConfig[deploymentEnv || 'staging'].find(
+ skuConfig => skuConfig.amount === `${donationAmount}`
+ );
+ return id;
+ };
+ const price = isSubscription
+ ? `${durationsConfig[donationDuration]}-donation-${donationAmount}`
+ : getSKUId();
+
+ /* eslint-disable camelcase */
+ try {
+ const session = await stripe.checkout.sessions.create({
+ payment_method_types: ['card'],
+ line_items: [
+ {
+ price,
+ quantity: 1
+ }
+ ],
+ metadata: { ...body },
+ mode: isSubscription ? 'subscription' : 'payment',
+ success_url: donationUrls.successUrl,
+ cancel_url: donationUrls.cancelUrl
+ });
+ /* eslint-enable camelcase */
+ return res.status(200).json({ id: session.id });
+ } catch (err) {
+ log(err.message);
+ return res.status(500).send({
+ type: 'danger',
+ message: 'Something went wrong.'
+ });
+ }
+ }
+
function updatePaypal(req, res) {
const { headers, body } = req;
return Promise.resolve(req)
@@ -284,6 +335,7 @@ export default function donateBoot(app, done) {
done();
} else {
api.post('/charge-stripe', createStripeDonation);
+ api.post('/create-stripe-session', createStripeSession);
api.post('/add-donation', addDonation);
hooks.post('/update-paypal', updatePaypal);
donateRouter.use('/donate', api);
diff --git a/api-server/src/server/middlewares/request-authorization.js b/api-server/src/server/middlewares/request-authorization.js
index a602d6d373..28b6367773 100644
--- a/api-server/src/server/middlewares/request-authorization.js
+++ b/api-server/src/server/middlewares/request-authorization.js
@@ -24,6 +24,7 @@ const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/;
+const createStripeSession = /^\/donate\/create-stripe-session/;
// note: this would be replaced by webhooks later
const donateRE = /^\/donate\/charge-stripe$/;
@@ -41,7 +42,8 @@ const _pathsAllowedREs = [
unsubscribedRE,
unsubscribeRE,
updateHooksRE,
- donateRE
+ donateRE,
+ createStripeSession
];
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {
diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index f6a7bdacf0..772921a8d2 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -289,6 +289,7 @@
"donate": {
"title": "Support our nonprofit",
"processing": "We are processing your donation.",
+ "redirecting": "Redirecting...",
"thanks": "Thanks for donating",
"thank-you": "Thank you for being a supporter.",
"thank-you-2": "Thank you for being a supporter of freeCodeCamp. You currently have a recurring donation.",
diff --git a/client/src/components/Donation/DonateCompletion.js b/client/src/components/Donation/DonateCompletion.js
index 148a22d1c3..107f74c570 100644
--- a/client/src/components/Donation/DonateCompletion.js
+++ b/client/src/components/Donation/DonateCompletion.js
@@ -9,15 +9,25 @@ import './Donation.css';
const propTypes = {
error: PropTypes.string,
processing: PropTypes.bool,
+ redirecting: PropTypes.bool,
reset: PropTypes.func.isRequired,
success: PropTypes.bool
};
-function DonateCompletion({ processing, reset, success, error = null }) {
+function DonateCompletion({
+ processing,
+ reset,
+ success,
+ redirecting,
+ error = null
+}) {
/* eslint-disable no-nested-ternary */
const { t } = useTranslation();
- const style = processing ? 'info' : success ? 'success' : 'danger';
- const heading = processing
+ const style =
+ processing || redirecting ? 'info' : success ? 'success' : 'danger';
+ const heading = redirecting
+ ? `${t('donate.redirecting')}`
+ : processing
? `${t('donate.processing')}`
: success
? `${t('donate.thank-you')}`
@@ -28,7 +38,7 @@ function DonateCompletion({ processing, reset, success, error = null }) {
{heading}
- {processing && (
+ {(processing || redirecting) && (
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
const propTypes = {
addDonation: PropTypes.func,
+ createStripeSession: PropTypes.func,
defaultTheme: PropTypes.string,
donationFormState: PropTypes.object,
email: PropTypes.string,
@@ -84,7 +85,8 @@ const mapDispatchToProps = {
addDonation,
navigate,
postChargeStripe,
- updateDonationFormState
+ updateDonationFormState,
+ createStripeSession
};
class DonateForm extends Component {
@@ -215,39 +217,26 @@ class DonateForm extends Component {
}
async handleStripeCheckoutRedirect(e, paymentMethod) {
- const { stripe } = this.state;
- const { donationAmount, donationDuration } = this.state;
+ e.preventDefault();
+ const { stripe, donationAmount, donationDuration } = this.state;
+ const { handleProcessing, email } = this.props;
- this.props.handleProcessing(
+ handleProcessing(
donationDuration,
donationAmount,
`stripe (${paymentMethod}) button click`
);
- const isOneTime = donationDuration === 'onetime';
- const getSKUId = () => {
- const { id } = onetimeSKUConfig[deploymentEnv || 'staging'].find(
- skuConfig => skuConfig.amount === `${donationAmount}`
- );
- return id;
- };
-
- e.preventDefault();
- const item = isOneTime
- ? {
- sku: getSKUId(),
- quantity: 1
- }
- : {
- plan: `${this.durations[donationDuration]}-donation-${donationAmount}`,
- quantity: 1
- };
- const { error } = await stripe.redirectToCheckout({
- items: [item],
- successUrl: donationUrls.successUrl,
- cancelUrl: donationUrls.cancelUrl
+ this.props.createStripeSession({
+ stripe,
+ data: {
+ donationAmount,
+ donationDuration,
+ clickedPaymentMethod: paymentMethod,
+ email,
+ context: 'donate page'
+ }
});
- console.error(error);
}
renderAmountButtons(duration) {
@@ -351,7 +340,8 @@ class DonateForm extends Component {
id='confirm-donation-btn'
onClick={e => this.handleStripeCheckoutRedirect(e, 'credit card')}
>
- {t('donate.credit-card')}
+ {}
+ {t('donate.credit-card')}
- {processing &&
+ {(processing || redirecting) &&
this.renderCompletion({
processing,
+ redirecting,
success,
error,
reset: this.resetDonation
})}
-
+
{isMinimalForm
? this.renderModalForm(processing)
: this.renderPageForm(processing)}
diff --git a/client/src/redux/donation-saga.js b/client/src/redux/donation-saga.js
index e09a7634e0..c918d397fe 100644
--- a/client/src/redux/donation-saga.js
+++ b/client/src/redux/donation-saga.js
@@ -21,7 +21,11 @@ import {
types as appTypes
} from './';
-import { addDonation, postChargeStripe } from '../utils/ajax';
+import {
+ addDonation,
+ postChargeStripe,
+ postCreateStripeSession
+} from '../utils/ajax';
const defaultDonationError = `Something is not right. Please contact donors@freecodecamp.org`;
@@ -55,6 +59,24 @@ function* addDonationSaga({ payload }) {
}
}
+function* createStripeSessionSaga({ payload: { stripe, data } }) {
+ try {
+ const session = yield call(postCreateStripeSession, {
+ ...data,
+ location: window.location.href
+ });
+ stripe.redirectToCheckout({
+ sessionId: session.data.id
+ });
+ } catch (error) {
+ const err =
+ error.response && error.response.data
+ ? error.response.data.message
+ : defaultDonationError;
+ yield put(addDonationError(err));
+ }
+}
+
function* postChargeStripeSaga({ payload }) {
try {
yield call(postChargeStripe, payload);
@@ -72,6 +94,7 @@ export function createDonationSaga(types) {
return [
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
takeEvery(types.addDonation, addDonationSaga),
- takeLeading(types.postChargeStripe, postChargeStripeSaga)
+ takeLeading(types.postChargeStripe, postChargeStripeSaga),
+ takeLeading(types.createStripeSession, createStripeSessionSaga)
];
}
diff --git a/client/src/redux/index.js b/client/src/redux/index.js
index f77a2f2426..76d88ac205 100644
--- a/client/src/redux/index.js
+++ b/client/src/redux/index.js
@@ -31,6 +31,7 @@ export const defaultFetchState = {
};
export const defaultDonationFormState = {
+ redirecting: false,
processing: false,
success: false,
error: ''
@@ -81,6 +82,7 @@ export const types = createTypes(
'updateDonationFormState',
...createAsyncTypes('fetchUser'),
...createAsyncTypes('addDonation'),
+ ...createAsyncTypes('createStripeSession'),
...createAsyncTypes('postChargeStripe'),
...createAsyncTypes('fetchProfileForUser'),
...createAsyncTypes('acceptTerms'),
@@ -150,6 +152,8 @@ export const addDonation = createAction(types.addDonation);
export const addDonationComplete = createAction(types.addDonationComplete);
export const addDonationError = createAction(types.addDonationError);
+export const createStripeSession = createAction(types.createStripeSession);
+
export const postChargeStripe = createAction(types.postChargeStripe);
export const postChargeStripeComplete = createAction(
types.postChargeStripeComplete
@@ -400,6 +404,10 @@ export const reducer = handleActions(
...state,
donationFormState: { ...state.donationFormState, ...payload }
}),
+ [types.createStripeSession]: state => ({
+ ...state,
+ donationFormState: { ...defaultDonationFormState, redirecting: true }
+ }),
[types.addDonation]: state => ({
...state,
donationFormState: { ...defaultDonationFormState, processing: true }
diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js
index d411dac75a..fde6346251 100644
--- a/client/src/utils/ajax.js
+++ b/client/src/utils/ajax.js
@@ -68,6 +68,10 @@ export function addDonation(body) {
return post('/donate/add-donation', body);
}
+export function postCreateStripeSession(body) {
+ return post('/donate/create-stripe-session', body);
+}
+
export function putUpdateLegacyCert(body) {
return post('/update-my-projects', body);
}