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

@@ -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;