feat(api): stripe checkout integration (#41666)
* feat: add api stripe checkout integration Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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.",
|
||||
|
@ -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 }) {
|
||||
<b>{heading}</b>
|
||||
</h4>
|
||||
<div className='donation-completion-body'>
|
||||
{processing && (
|
||||
{(processing || redirecting) && (
|
||||
<Spinner
|
||||
className='user-state-spinner'
|
||||
color='#0a0a23'
|
||||
|
@ -20,7 +20,6 @@ import {
|
||||
durationsConfig,
|
||||
defaultAmount,
|
||||
defaultDonation,
|
||||
onetimeSKUConfig,
|
||||
donationUrls,
|
||||
modalDefaultDonation
|
||||
} from '../../../../config/donation-settings';
|
||||
@ -36,6 +35,7 @@ import {
|
||||
donationFormStateSelector,
|
||||
hardGoTo as navigate,
|
||||
addDonation,
|
||||
createStripeSession,
|
||||
postChargeStripe,
|
||||
updateDonationFormState,
|
||||
defaultDonationFormState,
|
||||
@ -44,13 +44,14 @@ import {
|
||||
|
||||
import './Donation.css';
|
||||
|
||||
const { stripePublicKey, deploymentEnv } = envData;
|
||||
const { stripePublicKey } = envData;
|
||||
|
||||
const numToCommas = num =>
|
||||
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')}
|
||||
>
|
||||
<b>{t('donate.credit-card')}</b>
|
||||
{}
|
||||
<b>{t('donate.credit-card')} </b>
|
||||
</Button>
|
||||
<PaypalButton
|
||||
addDonation={addDonation}
|
||||
@ -430,29 +420,31 @@ class DonateForm extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
donationFormState: { processing, success, error },
|
||||
donationFormState: { processing, success, error, redirecting },
|
||||
isMinimalForm
|
||||
} = this.props;
|
||||
if (success || error) {
|
||||
return this.renderCompletion({
|
||||
processing,
|
||||
redirecting,
|
||||
success,
|
||||
error,
|
||||
reset: this.resetDonation
|
||||
});
|
||||
}
|
||||
|
||||
// keep payment provider elements on DOM during processing to avoid errors.
|
||||
// keep payment provider elements on DOM during processing and redirect to avoid errors.
|
||||
return (
|
||||
<>
|
||||
{processing &&
|
||||
{(processing || redirecting) &&
|
||||
this.renderCompletion({
|
||||
processing,
|
||||
redirecting,
|
||||
success,
|
||||
error,
|
||||
reset: this.resetDonation
|
||||
})}
|
||||
<div className={processing ? 'hide' : ''}>
|
||||
<div className={processing || redirecting ? 'hide' : ''}>
|
||||
{isMinimalForm
|
||||
? this.renderModalForm(processing)
|
||||
: this.renderPageForm(processing)}
|
||||
|
@ -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)
|
||||
];
|
||||
}
|
||||
|
@ -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 }
|
||||
|
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user