fix(client): replace Stripe with PayPal (#41924)

* feat: remove stripe payment option from client

* feat: remove stripe completely

* fix: remove last Stripe remnants

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2021-05-03 11:45:23 +03:00
committed by GitHub
parent 23564eb732
commit 27c8d564e4
32 changed files with 95 additions and 908 deletions

View File

@ -16574,24 +16574,6 @@
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true
},
"stripe": {
"version": "6.36.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-6.36.0.tgz",
"integrity": "sha512-7vjyVO4NCWvX38CH1AuSQH16uRxcQN+UhUTBPs4UHsIl5+SJXLBvCsHrMgd+bY9k1YDliT0fQB1fH9OI3GrEhw==",
"requires": {
"lodash.isplainobject": "^4.0.6",
"qs": "^6.6.0",
"safe-buffer": "^5.1.1",
"uuid": "^3.3.2"
},
"dependencies": {
"qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
}
}
},
"strong-error-handler": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-3.5.0.tgz",

View File

@ -69,7 +69,6 @@
"passport-mock-strategy": "^2.0.0",
"query-string": "^6.14.0",
"rx": "^4.1.0",
"stripe": "^6.36.0",
"uuid": "^3.4.0",
"validator": "^9.4.1"
},

View File

@ -1,231 +1,19 @@
import Stripe from 'stripe';
import debug from 'debug';
import { isEmail, isNumeric } from 'validator';
import {
getAsyncPaypalToken,
verifyWebHook,
updateUser,
verifyWebHookType
} from '../utils/donation';
import {
durationKeysConfig,
donationOneTimeConfig,
donationSubscriptionConfig,
durationsConfig,
onetimeSKUConfig,
donationUrls
} from '../../../../config/donation-settings';
import keys from '../../../../config/secrets';
import { deploymentEnv } from '../../../../config/env';
const log = debug('fcc:boot:donate');
export default function donateBoot(app, done) {
let stripe = false;
const { User } = app.models;
const api = app.loopback.Router();
const hooks = app.loopback.Router();
const donateRouter = app.loopback.Router();
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) &&
durationKeysConfig.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, stripePlans) {
if (err) {
throw err;
}
const requiredPlans = subscriptionPlans.map(plan => plan.id);
const availablePlans = stripePlans.data.map(plan => plan.id);
if (process.env.STRIPE_CREATE_PLANS === 'true') {
requiredPlans.forEach(requiredPlan => {
if (!availablePlans.includes(requiredPlan)) {
createStripePlan(
subscriptionPlans.find(plan => plan.id === requiredPlan)
);
}
});
} else {
log(`Skipping plan creation`);
}
});
resolve();
});
}
function createStripePlan(plan) {
log(`Creating subscription plan: ${plan.product.name}`);
stripe.plans.create(plan, function (err) {
if (err) {
log(err);
}
log(`Created plan with plan id: ${plan.id}`);
return;
});
}
function createStripeDonation(req, res) {
const { user, body } = req;
const {
amount,
duration,
token: { email, id }
} = body;
if (!validStripeForm(amount, duration, email)) {
return res.status(500).send({
error: 'The donation form had invalid values for this submission.'
});
}
const fccUser = user
? Promise.resolve(user)
: new Promise((resolve, reject) =>
User.findOrCreate(
{ where: { email } },
{ email },
(err, instance, isNew) => {
log('createing a new donating user instance: ', isNew);
if (err) {
return reject(err);
}
return resolve(instance);
}
)
);
let donatingUser = {};
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 Promise.resolve(fccUser)
.then(nonDonatingUser => {
const { isDonating } = nonDonatingUser;
if (isDonating && duration !== 'onetime') {
throw {
message: `User already has active recurring donation(s).`,
type: 'AlreadyDonatingError'
};
}
return nonDonatingUser;
})
.then(createCustomer)
.then(customer => {
return duration === 'onetime'
? 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' ||
err.type === 'AlreadyDonatingError'
) {
return res.status(402).send({ error: err.message });
}
return res
.status(500)
.send({ error: 'Donation failed due to a server error.' });
});
}
function addDonation(req, res) {
const { user, body } = req;
@ -250,53 +38,6 @@ 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)
@ -311,13 +52,8 @@ export default function donateBoot(app, done) {
.finally(() => res.status(200).json({ message: 'received paypal hook' }));
}
const stripeKey = keys.stripe.public;
const secKey = keys.stripe.secret;
const paypalKey = keys.paypal.client;
const paypalSec = keys.paypal.secret;
const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard';
const stripPublicInvalid =
!stripeKey || stripeKey === 'pk_from_stripe_dashboard';
const paypalSecretInvalid =
!paypalKey || paypalKey === 'id_from_paypal_dashboard';
@ -325,22 +61,19 @@ export default function donateBoot(app, done) {
!paypalSec || paypalSec === 'secret_from_paypal_dashboard';
const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid;
const stripeInvalid = stripeSecretInvalid || stripPublicInvalid;
if (stripeInvalid || paypalInvalid) {
if (paypalInvalid) {
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
throw new Error('Donation API keys are required to boot the server!');
}
log('Donation disabled in development unless ALL test keys are provided');
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);
donateRouter.use('/hooks', hooks);
app.use(donateRouter);
connectToStripe().then(done);
done();
}
}

View File

@ -12,9 +12,7 @@ export default function getCsurf() {
const { path } = req;
if (
// eslint-disable-next-line max-len
/^\/hooks\/update-paypal$|^\/hooks\/update-stripe$|^\/donate\/charge-stripe$/.test(
path
)
/^\/hooks\/update-paypal$/.test(path)
) {
return next();
}

View File

@ -23,11 +23,7 @@ const signinRE = /^\/signin/;
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$/;
const updateHooksRE = /^\/hooks\/update-paypal$/;
const _pathsAllowedREs = [
authRE,
@ -41,9 +37,7 @@ const _pathsAllowedREs = [
statusRE,
unsubscribedRE,
unsubscribeRE,
updateHooksRE,
donateRE,
createStripeSession
updateHooksRE
];
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {

View File

@ -47,7 +47,7 @@ describe('request-authorization', () => {
const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/;
const updateHooksRE = /^\/hooks\/update-paypal$/;
const allowedPathsList = [
authRE,
@ -79,11 +79,9 @@ describe('request-authorization', () => {
allowedPathsList
);
const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList);
const resultD = isAllowedPath('/hooks/update-stripe', allowedPathsList);
expect(resultA).toBe(true);
expect(resultB).toBe(true);
expect(resultC).toBe(true);
expect(resultD).toBe(true);
});
it('returns false for a non-white-listed path', () => {

View File

@ -9,7 +9,6 @@ ARG FORUM_LOCATION
ARG NEWS_LOCATION
ARG CLIENT_LOCALE
ARG CURRICULUM_LOCALE
ARG STRIPE_PUBLIC_KEY
ARG ALGOLIA_APP_ID
ARG ALGOLIA_API_KEY
ARG PAYPAL_CLIENT_ID

View File

@ -53,15 +53,6 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
}
}
if (!env.stripePublicKey) {
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
throw new Error('Stripe public key is required to start the client!');
} else {
reporter.info(
'Stripe public key missing or invalid. Required for donations.'
);
}
}
const { createPage } = actions;
return new Promise((resolve, reject) => {
@ -186,8 +177,7 @@ exports.onCreateWebpackConfig = ({ stage, plugins, actions }) => {
plugins.define({
HOME_PATH: JSON.stringify(
process.env.HOME_PATH || 'http://localhost:3000'
),
STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || '')
)
}),
// We add the shims of the node globals to the global scope
new webpack.ProvidePlugin({

View File

@ -297,6 +297,7 @@
"help-more": "Help us do more",
"error": "Something went wrong with your donation.",
"free-tech": "Your donations will support free technology education for people all over the world.",
"no-halo": "If you don't see a gold halo around your profile picture, contact donors@freecodecamp.org.",
"gift-frequency": "Select gift frequency:",
"gift-amount": "Select gift amount:",
"confirm": "Confirm your donation",

View File

@ -3224,14 +3224,6 @@
"@sinonjs/commons": "^1.7.0"
}
},
"@stripe/react-stripe-js": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.4.0.tgz",
"integrity": "sha512-Pz5QmG8PgJ3pi8gOWxlngk+ns63p2L1Ds192fn55ykZNRKfGz3G6sfssUVThHn/NAt2Hp1eCEsy/hvlKnXJI6g==",
"requires": {
"prop-types": "^15.7.2"
}
},
"@szmarczak/http-timer": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",

View File

@ -52,7 +52,6 @@
"@freecodecamp/react-calendar-heatmap": "1.0.0",
"@loadable/component": "5.14.1",
"@reach/router": "1.3.4",
"@stripe/react-stripe-js": "1.4.0",
"algoliasearch": "3.35.1",
"assert": "2.0.0",
"axios": "0.21.1",

View File

@ -168,11 +168,7 @@ const ShowCertification = props => {
setIsDonationClosed(true);
};
const handleProcessing = (
duration,
amount,
action = 'stripe form submission'
) => {
const handleProcessing = (duration, amount, action) => {
props.executeGA({
type: 'event',
data: {

View File

@ -49,6 +49,7 @@ function DonateCompletion({
{success && (
<div>
<p>{t('donate.free-tech')}</p>
<p>{t('donate.no-halo')}</p>
</div>
)}
{error && <p>{error}</p>}

View File

@ -3,9 +3,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Elements } from '@stripe/react-stripe-js';
import {
Button,
Col,
Row,
Tab,
@ -20,23 +18,16 @@ import {
durationsConfig,
defaultAmount,
defaultDonation,
donationUrls,
modalDefaultDonation
} from '../../../../config/donation-settings';
import envData from '../../../../config/env.json';
import { stripeScriptLoader } from '../../utils/scriptLoaders';
import Spacer from '../helpers/Spacer';
import PaypalButton from './PaypalButton';
import DonateCompletion from './DonateCompletion';
import StripeCardForm from './StripeCardForm';
import {
isSignedInSelector,
signInLoadingSelector,
donationFormStateSelector,
hardGoTo as navigate,
addDonation,
createStripeSession,
postChargeStripe,
updateDonationFormState,
defaultDonationFormState,
userSelector
@ -44,14 +35,11 @@ import {
import './Donation.css';
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,
@ -59,8 +47,6 @@ const propTypes = {
isDonating: PropTypes.bool,
isMinimalForm: PropTypes.bool,
isSignedIn: PropTypes.bool,
navigate: PropTypes.func.isRequired,
postChargeStripe: PropTypes.func.isRequired,
showLoading: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
theme: PropTypes.string,
@ -83,10 +69,7 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = {
addDonation,
navigate,
postChargeStripe,
updateDonationFormState,
createStripeSession
updateDonationFormState
};
class DonateForm extends Component {
@ -102,63 +85,26 @@ class DonateForm extends Component {
this.state = {
...initialAmountAndDuration,
processing: false,
stripe: null
processing: false
};
this.handleStripeLoad = this.handleStripeLoad.bind(this);
this.onDonationStateChange = this.onDonationStateChange.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.handleStripeCheckoutRedirect = this.handleStripeCheckoutRedirect.bind(
this
);
this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this);
this.resetDonation = this.resetDonation.bind(this);
this.postStripeDonation = this.postStripeDonation.bind(this);
}
componentDidMount() {
if (window.Stripe) {
this.handleStripeLoad();
} else if (document.querySelector('#stripe-js')) {
document
.querySelector('#stripe-js')
.addEventListener('load', this.handleStripeLoad);
} else {
stripeScriptLoader(this.handleStripeLoad);
}
}
componentWillUnmount() {
const stripeMountPoint = document.querySelector('#stripe-js');
if (stripeMountPoint) {
stripeMountPoint.removeEventListener('load', this.handleStripeLoad);
}
this.resetDonation();
}
handleStripeLoad() {
// Create Stripe instance once Stripe.js loads
if (stripePublicKey) {
this.setState(state => ({
...state,
stripe: window.Stripe(stripePublicKey)
}));
}
}
onDonationStateChange(donationState) {
// scroll to top
window.scrollTo(0, 0);
this.props.updateDonationFormState(donationState);
// send donation made on the donate page to related news article
if (donationState.success && !this.props.isMinimalForm) {
this.props.navigate(donationUrls.successUrl);
}
}
getActiveDonationAmount(durationSelected, amountSelected) {
@ -204,41 +150,6 @@ class DonateForm extends Component {
this.setState({ donationAmount });
}
postStripeDonation(token) {
const { donationAmount: amount, donationDuration: duration } = this.state;
window.scrollTo(0, 0);
// change the donation modal button label to close
// or display the close button for the cert donation section
if (this.props.handleProcessing) {
this.props.handleProcessing(duration, amount);
}
this.props.postChargeStripe({ token, amount, duration });
}
async handleStripeCheckoutRedirect(e, paymentMethod) {
e.preventDefault();
const { stripe, donationAmount, donationDuration } = this.state;
const { handleProcessing, email } = this.props;
handleProcessing(
donationDuration,
donationAmount,
`stripe (${paymentMethod}) button click`
);
this.props.createStripeSession({
stripe,
data: {
donationAmount,
donationDuration,
clickedPaymentMethod: paymentMethod,
email,
context: 'donate page'
}
});
}
renderAmountButtons(duration) {
return this.amounts[duration].map(amount => (
<ToggleButton
@ -318,7 +229,14 @@ class DonateForm extends Component {
}
renderDonationOptions() {
const { handleProcessing, isSignedIn, addDonation, t } = this.props;
const {
handleProcessing,
isSignedIn,
addDonation,
t,
defaultTheme,
theme
} = this.props;
const { donationAmount, donationDuration } = this.state;
const isOneTime = donationDuration === 'onetime';
@ -334,15 +252,6 @@ class DonateForm extends Component {
)}
<Spacer />
<div className='donate-btn-group'>
<Button
block={true}
bsStyle='primary'
id='confirm-donation-btn'
onClick={e => this.handleStripeCheckoutRedirect(e, 'credit card')}
>
{}
<b>{t('donate.credit-card')} </b>
</Button>
<PaypalButton
addDonation={addDonation}
donationAmount={donationAmount}
@ -351,6 +260,7 @@ class DonateForm extends Component {
isSubscription={isOneTime ? false : true}
onDonationStateChange={this.onDonationStateChange}
skipAddDonation={!isSignedIn}
theme={defaultTheme ? defaultTheme : theme}
/>
</div>
</div>
@ -366,22 +276,13 @@ class DonateForm extends Component {
}
renderModalForm() {
const { donationAmount, donationDuration, stripe } = this.state;
const {
handleProcessing,
addDonation,
email,
theme,
t,
defaultTheme
} = this.props;
const { donationAmount, donationDuration } = this.state;
const { handleProcessing, addDonation, defaultTheme, theme } = this.props;
return (
<Row>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer />
<b>
{this.getDonationButtonLabel()} {t('donate.paypal')}
</b>
<b>{this.getDonationButtonLabel()}:</b>
<Spacer />
<PaypalButton
addDonation={addDonation}
@ -389,22 +290,9 @@ class DonateForm extends Component {
donationDuration={donationDuration}
handleProcessing={handleProcessing}
onDonationStateChange={this.onDonationStateChange}
theme={defaultTheme ? defaultTheme : theme}
/>
</Col>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer />
<b>{t('donate.credit-card-2')}</b>
<Spacer />
<Elements stripe={stripe}>
<StripeCardForm
getDonationButtonLabel={this.getDonationButtonLabel}
onDonationStateChange={this.onDonationStateChange}
postStripeDonation={this.postStripeDonation}
theme={defaultTheme ? defaultTheme : theme}
userEmail={email}
/>
</Elements>
</Col>
</Row>
);
}

View File

@ -48,6 +48,10 @@
font-weight: 700;
}
.paypal-buttons-container {
min-height: 142px;
}
.donate-input-element {
padding-top: 8px;
}
@ -56,8 +60,7 @@
color: #707070;
}
.donation-form .form-control:focus,
.StripeElement--focus {
.donation-form .form-control:focus {
border-color: #66afe9;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
@ -66,7 +69,7 @@
.donation-form .email--invalid.form-control:focus,
.donation-form .email--invalid,
.donation-form .StripeElement--invalid {
.donation-form {
border-color: #eb1c26;
color: #eb1c26;
}
@ -383,7 +386,8 @@ button#confirm-donation-btn:hover {
.donate-btn-group {
display: flex;
flex-direction: column;
flex-direction: row;
justify-content: center;
}
.donate-btn-group > * {

View File

@ -55,11 +55,7 @@ function DonateModal({
}) {
const [closeLabel, setCloseLabel] = React.useState(false);
const { t } = useTranslation();
const handleProcessing = (
duration,
amount,
action = 'stripe form submission'
) => {
const handleProcessing = (duration, amount, action) => {
executeGA({
type: 'event',
data: {

View File

@ -25,7 +25,10 @@ export class PayPalButtonScriptLoader extends Component {
}
componentDidUpdate(prevProps) {
if (prevProps.isSubscription !== this.state.isSubscription) {
if (
prevProps.isSubscription !== this.state.isSubscription ||
prevProps.style !== this.props.style
) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ isSdkLoaded: false });
this.loadScript(this.state.isSubscription, true);
@ -34,7 +37,7 @@ export class PayPalButtonScriptLoader extends Component {
loadScript(subscription, deleteScript) {
if (deleteScript) scriptRemover('paypal-sdk');
let queries = `?client-id=${this.props.clientId}&disable-funding=credit,card,bancontact,blik,eps,giropay,ideal,mybank,p24,sepa,sofort,venmo`;
let queries = `?client-id=${this.props.clientId}&disable-funding=credit,bancontact,blik,eps,giropay,ideal,mybank,p24,sepa,sofort,venmo`;
if (subscription) queries += '&vault=true&intent=subscription';
scriptLoader(

View File

@ -58,58 +58,61 @@ export class PaypalButton extends Component {
render() {
const { duration, planId, amount } = this.state;
const { t } = this.props;
const { t, theme } = this.props;
const isSubscription = duration !== 'onetime';
const buttonColor = theme === 'night' ? 'white' : 'gold';
if (!paypalClientId) {
return null;
}
return (
<PayPalButtonScriptLoader
amount={amount}
clientId={paypalClientId}
createOrder={(data, actions) => {
return actions.order.create({
purchase_units: [
{
amount: {
currency_code: 'USD',
value: (amount / 100).toString()
<div className={'paypal-buttons-container'}>
<PayPalButtonScriptLoader
amount={amount}
clientId={paypalClientId}
createOrder={(data, actions) => {
return actions.order.create({
purchase_units: [
{
amount: {
currency_code: 'USD',
value: (amount / 100).toString()
}
}
}
]
});
}}
createSubscription={(data, actions) => {
return actions.subscription.create({
plan_id: planId
});
}}
isSubscription={isSubscription}
onApprove={data => {
this.handleApproval(data, isSubscription);
}}
onCancel={() => {
this.props.onDonationStateChange({
processing: false,
success: false,
error: t('donate.failed-pay')
});
}}
onError={() =>
this.props.onDonationStateChange({
processing: false,
success: false,
error: t('donate.try-again')
})
}
plantId={planId}
style={{
tagline: false,
height: 43
}}
/>
]
});
}}
createSubscription={(data, actions) => {
return actions.subscription.create({
plan_id: planId
});
}}
isSubscription={isSubscription}
onApprove={data => {
this.handleApproval(data, isSubscription);
}}
onCancel={() => {
this.props.onDonationStateChange({
processing: false,
success: false,
error: t('donate.failed-pay')
});
}}
onError={() =>
this.props.onDonationStateChange({
processing: false,
success: false,
error: t('donate.try-again')
})
}
plantId={planId}
style={{
tagline: false,
height: 43,
color: buttonColor
}}
/>
</div>
);
}
}
@ -122,7 +125,8 @@ const propTypes = {
isDonating: PropTypes.bool,
onDonationStateChange: PropTypes.func,
skipAddDonation: PropTypes.bool,
t: PropTypes.func.isRequired
t: PropTypes.func.isRequired,
theme: PropTypes.string
};
const mapStateToProps = createSelector(

View File

@ -1,198 +0,0 @@
import React, { useState } from 'react';
import {
CardNumberElement,
CardExpiryElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
import PropTypes from 'prop-types';
import isEmail from 'validator/lib/isEmail';
import {
Row,
Col,
ControlLabel,
FormGroup,
Image,
Button,
Form,
FormControl,
Alert
} from '@freecodecamp/react-bootstrap';
import { withTranslation } from 'react-i18next';
const initialPaymentInfoValidityState = {
cardNumber: {
complete: false,
error: null
},
cardExpiry: {
complete: false,
error: null
}
};
const propTypes = {
getDonationButtonLabel: PropTypes.func.isRequired,
onDonationStateChange: PropTypes.func,
postStripeDonation: PropTypes.func,
t: PropTypes.func.isRequired,
theme: PropTypes.string,
userEmail: PropTypes.string
};
const StripeCardForm = ({
getDonationButtonLabel,
theme,
t,
onDonationStateChange,
postStripeDonation,
userEmail
}) => {
const [isSubmissionValid, setSubmissionValidity] = useState(true);
const [email, setEmail] = useState(userEmail);
const [isEmailValid, setEmailValidity] = useState(true);
const [paymentInfoValidation, setPaymentValidity] = useState(
initialPaymentInfoValidityState
);
const stripe = useStripe();
const elements = useElements();
function handleInputChange(event) {
const { elementType, error, complete } = event;
setPaymentValidity({
...paymentInfoValidation,
[elementType]: {
error,
complete
}
});
}
function isPaymentInfoValid() {
return Object.keys(paymentInfoValidation)
.map(key => paymentInfoValidation[key])
.every(({ complete, error }) => complete && !error);
}
const options = {
style: {
base: {
fontSize: '18px',
color: `${theme === 'night' ? '#fff' : '#0a0a23'}`
}
}
};
const handleSubmit = async event => {
event.preventDefault();
if (!isEmailValid || !isPaymentInfoValid())
return setSubmissionValidity(false);
else setSubmissionValidity(true);
if (!isEmail(email)) {
return onDonationStateChange({
error: t('donate.need-email')
});
}
const { error, token } = await stripe.createToken(
elements.getElement(CardNumberElement),
{ email }
);
if (error) {
return onDonationStateChange({
error: t('donate.went-wrong')
});
}
return postStripeDonation(token);
};
const handleEmailChange = e => {
const newValue = e.target.value;
setEmail(newValue);
setEmailValidity(true);
};
const handleEmailBlur = () => {
const newValidation = isEmail(email);
setEmailValidity(newValidation);
};
const renderErrorMessage = () => {
let message = '';
if (!isEmailValid && !isPaymentInfoValid())
message = <p>{t('donate.valid-info')}</p>;
else if (!isEmailValid) message = <p>{t('donate.valid-email')}</p>;
else message = <p>{t('donate.valid-card')}</p>;
return <Alert bsStyle='danger'>{message}</Alert>;
};
return (
<Form className='donation-form' onSubmit={handleSubmit}>
<div>{!isSubmissionValid ? renderErrorMessage() : ''}</div>
<FormGroup className='donation-email-container'>
<ControlLabel>{t('donate.email-receipt')}</ControlLabel>
<FormControl
className={!isEmailValid && email ? 'email--invalid' : ''}
key='3'
onBlur={handleEmailBlur}
onChange={handleEmailChange}
placeholder='me@example.com'
required={true}
type='text'
value={email || ''}
/>
</FormGroup>
<div className='donation-elements'>
<FormGroup>
<ControlLabel>{t('donate.card-number')}</ControlLabel>
<CardNumberElement
className='form-control donate-input-element'
onChange={handleInputChange}
options={options}
/>
</FormGroup>
<FormGroup>
<ControlLabel>{t('donate.expiration')}</ControlLabel>
<Row>
<Col md={5} xs={12}>
<CardExpiryElement
className='form-control donate-input-element'
onChange={handleInputChange}
options={options}
/>
</Col>
<Col className='form-payments-wrapper' md={7} xs={12}>
<Image
alt='payment options'
className='form-payment-methods'
src={
'https://cdn.freecodecamp.org' +
'/platform/universal/form-payments.png'
}
/>
</Col>
</Row>
</FormGroup>
</div>
<Button
block={true}
bsStyle='primary'
disabled={!stripe}
id='confirm-donation-btn'
type='submit'
>
{getDonationButtonLabel()}
</Button>
</Form>
);
};
StripeCardForm.displayName = 'StripeCardForm';
StripeCardForm.propTypes = propTypes;
export default withTranslation()(StripeCardForm);

View File

@ -2,7 +2,6 @@ import React from 'react';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
import stripeObserver from './stripeIframesFix';
import UniversalNav from './components/UniversalNav';
import './header.css';
@ -26,10 +25,6 @@ export class Header extends React.Component {
componentDidMount() {
document.addEventListener('click', this.handleClickOutside);
// Remove stacking Stripe iframes with each navigation
// after visiting /donate
stripeObserver();
}
componentWillUnmount() {

View File

@ -1,30 +0,0 @@
const stripeObserver = () => {
const config = { attributes: false, childList: true, subtree: false };
const filterNodes = nl =>
Array.from(nl)
.filter(b => b.nodeName === 'IFRAME')
.filter(b => /__privateStripeController/.test(b.name));
const mutationCallback = a => {
const controllerAdded = a.reduce(
(acc, curr) =>
curr.type === 'childList'
? [...acc, ...filterNodes(curr.addedNodes)]
: acc,
[]
)[0];
if (controllerAdded) {
const allControllers = filterNodes(document.body.childNodes);
allControllers.forEach(controller => {
if (controller.name !== controllerAdded.name) {
controller.remove();
}
});
}
};
return new MutationObserver(mutationCallback).observe(document.body, config);
};
export default stripeObserver;

View File

@ -62,7 +62,7 @@ class DonatePage extends Component {
});
}
handleProcessing(duration, amount, action = 'stripe button click') {
handleProcessing(duration, amount, action) {
this.props.executeGA({
type: 'event',
data: {

View File

@ -1,12 +1,4 @@
import {
put,
select,
takeEvery,
takeLeading,
delay,
call,
take
} from 'redux-saga/effects';
import { put, select, takeEvery, delay, call, take } from 'redux-saga/effects';
import {
openDonationModal,
@ -16,16 +8,10 @@ import {
recentlyClaimedBlockSelector,
addDonationComplete,
addDonationError,
postChargeStripeComplete,
postChargeStripeError,
types as appTypes
} from './';
import {
addDonation,
postChargeStripe,
postCreateStripeSession
} from '../utils/ajax';
import { addDonation } from '../utils/ajax';
const defaultDonationError = `Something is not right. Please contact donors@freecodecamp.org`;
@ -59,42 +45,9 @@ 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);
yield put(postChargeStripeComplete());
} catch (error) {
const err =
error.response && error.response.data
? error.response.data.error
: defaultDonationError;
yield put(postChargeStripeError(err));
}
}
export function createDonationSaga(types) {
return [
takeEvery(types.tryToShowDonationModal, showDonateModalSaga),
takeEvery(types.addDonation, addDonationSaga),
takeLeading(types.postChargeStripe, postChargeStripeSaga),
takeLeading(types.createStripeSession, createStripeSessionSaga)
takeEvery(types.addDonation, addDonationSaga)
];
}

View File

@ -82,8 +82,6 @@ export const types = createTypes(
'updateDonationFormState',
...createAsyncTypes('fetchUser'),
...createAsyncTypes('addDonation'),
...createAsyncTypes('createStripeSession'),
...createAsyncTypes('postChargeStripe'),
...createAsyncTypes('fetchProfileForUser'),
...createAsyncTypes('acceptTerms'),
...createAsyncTypes('showCert'),
@ -152,14 +150,6 @@ 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
);
export const postChargeStripeError = createAction(types.postChargeStripeError);
export const fetchProfileForUser = createAction(types.fetchProfileForUser);
export const fetchProfileForUserComplete = createAction(
types.fetchProfileForUserComplete
@ -404,10 +394,6 @@ 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 }
@ -431,29 +417,6 @@ export const reducer = handleActions(
...state,
donationFormState: { ...defaultDonationFormState, error: payload }
}),
[types.postChargeStripe]: state => ({
...state,
donationFormState: { ...defaultDonationFormState, processing: true }
}),
[types.postChargeStripeComplete]: state => {
const { appUsername } = state;
return {
...state,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
isDonating: true
}
},
donationFormState: { ...defaultDonationFormState, success: true }
};
},
[types.postChargeStripeError]: (state, { payload }) => ({
...state,
donationFormState: { ...defaultDonationFormState, error: payload }
}),
[types.fetchUser]: state => ({
...state,
userFetchState: { ...defaultFetchState }

View File

@ -60,18 +60,11 @@ export function getArticleById(shortId) {
}
/** POST **/
export function postChargeStripe(body) {
return post('/donate/charge-stripe', body);
}
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);
}

View File

@ -17,15 +17,6 @@ export const scriptRemover = id => {
}
};
export const stripeScriptLoader = onload =>
scriptLoader(
'stripe-js',
'stripe-js',
false,
'https://js.stripe.com/v3/',
onload
);
export const mathJaxScriptLoader = () =>
scriptLoader(
'mathjax',

View File

@ -109,8 +109,6 @@ export const injectConditionalTags = (tagsArray, homeLocation) => {
export const getPostBodyComponents = pathname => {
let scripts = [];
const challengesPathRE = new RegExp('/learn/[^/]+/[^/]+/[^/]+/?$');
const donatePathRE = new RegExp('/donate/?$');
const mathJaxScriptElement = (
<script
async={false}
@ -120,15 +118,6 @@ export const getPostBodyComponents = pathname => {
type='text/javascript'
/>
);
const stripeScriptElement = (
<script
async={true}
id='stripe-js'
key='stripe-js'
src='https://js.stripe.com/v3/'
type='text/javascript'
/>
);
if (
pathname.includes('/learn/coding-interview-prep/rosetta-code') ||
@ -136,8 +125,6 @@ export const getPostBodyComponents = pathname => {
) {
scripts.push(mathJaxScriptElement);
}
if (challengesPathRE.test(pathname) || donatePathRE.test(pathname)) {
scripts.push(stripeScriptElement);
}
return scripts.filter(Boolean);
};

View File

@ -21,7 +21,6 @@ const {
CLIENT_LOCALE: clientLocale,
CURRICULUM_LOCALE: curriculumLocale,
SHOW_LOCALE_DROPDOWN_MENU: showLocaleDropdownMenu,
STRIPE_PUBLIC_KEY: stripePublicKey,
ALGOLIA_APP_ID: algoliaAppId,
ALGOLIA_API_KEY: algoliaAPIKey,
PAYPAL_CLIENT_ID: paypalClientId,
@ -45,10 +44,6 @@ module.exports = Object.assign(locations, {
showLocaleDropdownMenu: showLocaleDropdownMenu === 'true',
deploymentEnv,
environment: process.env.FREECODECAMP_NODE_ENV || 'development',
stripePublicKey:
!stripePublicKey || stripePublicKey === 'pk_from_stripe_dashboard'
? null
: stripePublicKey,
algoliaAppId:
!algoliaAppId || algoliaAppId === 'app_id_from_algolia_dashboard'
? null

View File

@ -29,9 +29,6 @@ const {
SENTRY_DSN,
STRIPE_PUBLIC_KEY,
STRIPE_SECRET_KEY,
PAYPAL_CLIENT_ID,
PAYPAL_SECRET,
PAYPAL_VERIFY_WEBHOOK_URL,
@ -95,11 +92,6 @@ module.exports = {
dns: SENTRY_DSN
},
stripe: {
public: STRIPE_PUBLIC_KEY,
secret: STRIPE_SECRET_KEY
},
paypal: {
client: PAYPAL_CLIENT_ID,
secret: PAYPAL_SECRET,

View File

@ -18,10 +18,6 @@ describe('The Document Metadata', () => {
};
const scripts = {
stripe: {
selector: 'body script[id="stripe-js"]',
src: 'https://js.stripe.com/v3/'
},
mathjax: {
selector: 'body script[id="mathjax"]',
src:
@ -63,10 +59,6 @@ describe('The Document Metadata', () => {
social.description
);
});
it('landing page should not have stripe body script', () => {
cy.reload();
cy.get(scripts.stripe.selector).should('not.exist');
});
it('landing page should not have mathjax body script', () => {
cy.reload();
cy.get(scripts.mathjax.selector).should('not.exist');
@ -76,24 +68,6 @@ describe('The Document Metadata', () => {
cy.reload();
cy.get(scripts.mathjax.selector).should('not.exist');
});
it('donate page should have stripe body script', () => {
cy.visit('/donate');
cy.reload();
cy.get(scripts.stripe.selector).should(
'have.attr',
'src',
scripts.stripe.src
);
});
it('responsive webdesign challenges should have stripe body script', () => {
cy.visit(challengs.responsiveWebDesign);
cy.reload();
cy.get(scripts.stripe.selector).should(
'have.attr',
'src',
scripts.stripe.src
);
});
it('project euler challenges should have mathjax body script', () => {
cy.visit(challengs.projectEuler);
cy.reload();

View File

@ -31,11 +31,6 @@ ALGOLIA_API_KEY=api_key_from_algolia_dashboard
# Donations
# ---------------------
# Stripe
STRIPE_CREATE_PLANS=true
STRIPE_PUBLIC_KEY=pk_from_stripe_dashboard
STRIPE_SECRET_KEY=sk_from_stripe_dashboard
# PayPal
PAYPAL_CLIENT_ID=id_from_paypal_dashboard
PAYPAL_SECRET=secret_from_paypal_dashboard

View File

@ -45,7 +45,7 @@ if (FREECODECAMP_NODE_ENV !== 'development') {
'showUpcomingChanges'
];
const searchKeys = ['algoliaAppId', 'algoliaAPIKey'];
const donationKeys = ['stripePublicKey', 'paypalClientId'];
const donationKeys = ['paypalClientId'];
const expectedVariables = locationKeys.concat(
deploymentKeys,