fix(client): modernize stripe form (#41359)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2021-03-10 18:03:55 +03:00
committed by GitHub
parent d8e6d8dc46
commit aac49e9a40
6 changed files with 204 additions and 30936 deletions

30658
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
"@freecodecamp/react-calendar-heatmap": "^1.0.0", "@freecodecamp/react-calendar-heatmap": "^1.0.0",
"@loadable/component": "^5.14.1", "@loadable/component": "^5.14.1",
"@reach/router": "^1.3.4", "@reach/router": "^1.3.4",
"@stripe/react-stripe-js": "^1.4.0",
"algoliasearch": "^3.35.1", "algoliasearch": "^3.35.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"babel-plugin-prismjs": "^2.0.1", "babel-plugin-prismjs": "^2.0.1",
@ -63,7 +64,6 @@
"react-responsive": "^6.1.1", "react-responsive": "^6.1.1",
"react-scrollable-anchor": "^0.6.1", "react-scrollable-anchor": "^0.6.1",
"react-spinkit": "^3.0.0", "react-spinkit": "^3.0.0",
"react-stripe-elements": "^2.0.3",
"react-tooltip": "^4.2.13", "react-tooltip": "^4.2.13",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"react-youtube": "^7.13.1", "react-youtube": "^7.13.1",

View File

@ -276,7 +276,7 @@ const ShowCertification = props => {
<Col md={8} mdOffset={2} xs={12}> <Col md={8} mdOffset={2} xs={12}>
<DonateForm <DonateForm
handleProcessing={handleProcessing} handleProcessing={handleProcessing}
defaultTheme='light' defaultTheme='default'
isMinimalForm={true} isMinimalForm={true}
/> />
</Col> </Col>

View File

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { StripeProvider, Elements } from 'react-stripe-elements'; import { Elements } from '@stripe/react-stripe-js';
import { import {
Button, Button,
Col, Col,
@ -26,10 +26,10 @@ import {
} from '../../../../config/donation-settings'; } from '../../../../config/donation-settings';
import { stripePublicKey, deploymentEnv } from '../../../../config/env.json'; import { stripePublicKey, deploymentEnv } from '../../../../config/env.json';
import { stripeScriptLoader } from '../../utils/scriptLoaders'; import { stripeScriptLoader } from '../../utils/scriptLoaders';
import DonateFormChildViewForHOC from './DonateFormChildViewForHOC';
import Spacer from '../helpers/Spacer'; import Spacer from '../helpers/Spacer';
import PaypalButton from './PaypalButton'; import PaypalButton from './PaypalButton';
import DonateCompletion from './DonateCompletion'; import DonateCompletion from './DonateCompletion';
import StripeCardForm from './StripeCardForm';
import { import {
isSignedInSelector, isSignedInSelector,
signInLoadingSelector, signInLoadingSelector,
@ -38,7 +38,8 @@ import {
addDonation, addDonation,
postChargeStripe, postChargeStripe,
updateDonationFormState, updateDonationFormState,
defaultDonationFormState defaultDonationFormState,
userSelector
} from '../../redux'; } from '../../redux';
import './Donation.css'; import './Donation.css';
@ -50,6 +51,7 @@ const propTypes = {
addDonation: PropTypes.func, addDonation: PropTypes.func,
defaultTheme: PropTypes.string, defaultTheme: PropTypes.string,
donationFormState: PropTypes.object, donationFormState: PropTypes.object,
email: PropTypes.string,
handleProcessing: PropTypes.func, handleProcessing: PropTypes.func,
isDonating: PropTypes.bool, isDonating: PropTypes.bool,
isMinimalForm: PropTypes.bool, isMinimalForm: PropTypes.bool,
@ -58,6 +60,7 @@ const propTypes = {
postChargeStripe: PropTypes.func.isRequired, postChargeStripe: PropTypes.func.isRequired,
showLoading: PropTypes.bool.isRequired, showLoading: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
theme: PropTypes.string,
updateDonationFormState: PropTypes.func updateDonationFormState: PropTypes.func
}; };
@ -65,10 +68,13 @@ const mapStateToProps = createSelector(
signInLoadingSelector, signInLoadingSelector,
isSignedInSelector, isSignedInSelector,
donationFormStateSelector, donationFormStateSelector,
(showLoading, isSignedIn, donationFormState) => ({ userSelector,
(showLoading, isSignedIn, donationFormState, { email, theme }) => ({
isSignedIn, isSignedIn,
showLoading, showLoading,
donationFormState donationFormState,
email,
theme
}) })
); );
@ -107,6 +113,7 @@ class DonateForm extends Component {
); );
this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this); this.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this);
this.resetDonation = this.resetDonation.bind(this); this.resetDonation = this.resetDonation.bind(this);
this.postStripeDonation = this.postStripeDonation.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -193,6 +200,18 @@ class DonateForm extends Component {
this.setState({ donationAmount }); 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) { async handleStripeCheckoutRedirect(e, paymentMethod) {
const { stripe } = this.state; const { stripe } = this.state;
const { donationAmount, donationDuration } = this.state; const { donationAmount, donationDuration } = this.state;
@ -358,12 +377,12 @@ class DonateForm extends Component {
const { donationAmount, donationDuration, stripe } = this.state; const { donationAmount, donationDuration, stripe } = this.state;
const { const {
handleProcessing, handleProcessing,
defaultTheme,
addDonation, addDonation,
postChargeStripe, email,
t theme,
t,
defaultTheme
} = this.props; } = this.props;
return ( return (
<Row> <Row>
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}> <Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
@ -384,19 +403,15 @@ class DonateForm extends Component {
<Spacer /> <Spacer />
<b>{t('donate.credit-card-2')}</b> <b>{t('donate.credit-card-2')}</b>
<Spacer /> <Spacer />
<StripeProvider stripe={stripe}> <Elements stripe={stripe}>
<Elements> <StripeCardForm
<DonateFormChildViewForHOC getDonationButtonLabel={this.getDonationButtonLabel}
defaultTheme={defaultTheme} onDonationStateChange={this.onDonationStateChange}
donationAmount={donationAmount} postStripeDonation={this.postStripeDonation}
donationDuration={donationDuration} theme={defaultTheme ? defaultTheme : theme}
getDonationButtonLabel={this.getDonationButtonLabel} userEmail={email}
handleProcessing={handleProcessing} />
onDonationStateChange={this.onDonationStateChange} </Elements>
postChargeStripe={postChargeStripe}
/>
</Elements>
</StripeProvider>
</Col> </Col>
</Row> </Row>
); );

View File

@ -1,211 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import isEmail from 'validator/lib/isEmail';
import {
Button,
ControlLabel,
Form,
FormControl,
FormGroup,
Alert
} from '@freecodecamp/react-bootstrap';
import { injectStripe } from 'react-stripe-elements';
import StripeCardForm from './StripeCardForm';
import DonateCompletion from './DonateCompletion';
import { userSelector } from '../../redux';
import { withTranslation } from 'react-i18next';
const propTypes = {
defaultTheme: PropTypes.string,
donationAmount: PropTypes.number.isRequired,
donationDuration: PropTypes.string.isRequired,
email: PropTypes.string,
getDonationButtonLabel: PropTypes.func.isRequired,
handleProcessing: PropTypes.func,
isSignedIn: PropTypes.bool,
onDonationStateChange: PropTypes.func,
postChargeStripe: PropTypes.func,
showCloseBtn: PropTypes.func,
stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired
}),
t: PropTypes.func.isRequired,
theme: PropTypes.string
};
const mapStateToProps = createSelector(
userSelector,
({ email, theme }) => ({ email, theme })
);
class DonateFormChildViewForHOC extends Component {
constructor(...args) {
super(...args);
this.state = {
isSubmissionValid: null,
email: null,
isEmailValid: true,
isFormValid: false
};
this.getValidationState = this.getValidationState.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postDonation = this.postDonation.bind(this);
this.handleEmailBlur = this.handleEmailBlur.bind(this);
}
getUserEmail() {
const { email: stateEmail } = this.state;
const { email: propsEmail } = this.props;
return stateEmail || propsEmail || '';
}
getValidationState(isFormValid) {
this.setState(state => ({
...state,
isFormValid
}));
}
handleEmailChange(e) {
const newValue = e.target.value;
return this.setState({
email: newValue,
// reset validation
isEmailValid: true
});
}
handleSubmit(e) {
e.preventDefault();
const { isEmailValid, isFormValid } = this.state;
if ((!isEmailValid, !isFormValid)) {
return this.setState({
isSubmissionValid: false
});
}
this.setState({
isSubmissionValid: null
});
const { t } = this.props;
const email = this.getUserEmail();
if (!email || !isEmail(email)) {
return this.props.onDonationStateChange({
error: t('donate.need-email')
});
}
return this.props.stripe.createToken({ email }).then(({ error, token }) => {
if (error) {
return this.props.onDonationStateChange({
error: t('donate.went-wrong')
});
}
return this.postDonation(token);
});
}
postDonation(token) {
const { donationAmount: amount, donationDuration: duration } = this.props;
// scroll to top
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);
}
return this.props.postChargeStripe({
token,
amount,
duration
});
}
renderCompletion(props) {
return <DonateCompletion {...props} />;
}
handleEmailBlur() {
const emailValue = this.state.email;
const newValidation = isEmail(emailValue);
return this.setState({
isEmailValid: newValidation
});
}
renderErrorMessage() {
const { isEmailValid, isFormValid } = this.state;
const { t } = this.props;
let message = '';
if (!isEmailValid && !isFormValid)
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>;
}
renderDonateForm() {
const { isEmailValid, isSubmissionValid, email } = this.state;
const { getDonationButtonLabel, theme, defaultTheme, t } = this.props;
return (
<Form className='donation-form' onSubmit={this.handleSubmit}>
<div>{isSubmissionValid !== null ? this.renderErrorMessage() : ''}</div>
<FormGroup className='donation-email-container'>
<ControlLabel>{t('donate.email-receipt')}</ControlLabel>
<FormControl
className={!isEmailValid && email ? 'email--invalid' : ''}
key='3'
onBlur={this.handleEmailBlur}
onChange={this.handleEmailChange}
placeholder='me@example.com'
required={true}
type='text'
value={this.state.email || ''}
/>
</FormGroup>
<StripeCardForm
getValidationState={this.getValidationState}
theme={defaultTheme ? defaultTheme : theme}
/>
<Button
block={true}
bsStyle='primary'
id='confirm-donation-btn'
type='submit'
>
{getDonationButtonLabel()}
</Button>
</Form>
);
}
componentWillReceiveProps({ email }) {
if (this.state.email === null && email) {
this.setState({ email });
}
}
render() {
return this.renderDonateForm();
}
}
DonateFormChildViewForHOC.displayName = 'DonateFormChildViewForHOC';
DonateFormChildViewForHOC.propTypes = propTypes;
export default injectStripe(
connect(mapStateToProps)(withTranslation()(DonateFormChildViewForHOC))
);

View File

@ -1,88 +1,159 @@
import React, { Component } from 'react'; import React, { useState } from 'react';
import {
CardNumberElement,
CardExpiryElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CardNumberElement, CardExpiryElement } from 'react-stripe-elements'; import isEmail from 'validator/lib/isEmail';
import { import {
Row, Row,
Col, Col,
ControlLabel, ControlLabel,
FormGroup, FormGroup,
Image Image,
Button,
Form,
FormControl,
Alert
} from '@freecodecamp/react-bootstrap'; } from '@freecodecamp/react-bootstrap';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
const initialPaymentInfoValidityState = {
cardNumber: {
complete: false,
error: null
},
cardExpiry: {
complete: false,
error: null
}
};
const propTypes = { const propTypes = {
getValidationState: PropTypes.func.isRequired, getDonationButtonLabel: PropTypes.func.isRequired,
onDonationStateChange: PropTypes.func,
postStripeDonation: PropTypes.func,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
theme: PropTypes.string theme: PropTypes.string,
userEmail: PropTypes.string
}; };
const style = { const StripeCardForm = ({
base: { getDonationButtonLabel,
fontSize: '18px' 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
);
class StripeCardForm extends Component { const stripe = useStripe();
constructor(...props) { const elements = useElements();
super(...props);
this.state = { function handleInputChange(event) {
validation: {
cardNumber: {
complete: false,
error: null
},
cardExpiry: {
complete: false,
error: null
}
}
};
this.handleInputChange = this.handleInputChange.bind(this);
this.isValidInput = this.isValidInput.bind(this);
}
componentDidMount() {
this.props.getValidationState(this.isValidInput());
}
handleInputChange(event) {
const { elementType, error, complete } = event; const { elementType, error, complete } = event;
return this.setState( setPaymentValidity({
state => ({ ...paymentInfoValidation,
...state, [elementType]: {
validation: { error,
...state.validation, complete
[elementType]: { }
error, });
complete
}
}
}),
() => this.props.getValidationState(this.isValidInput())
);
} }
isValidInput() { function isPaymentInfoValid() {
const { validation } = this.state; return Object.keys(paymentInfoValidation)
return Object.keys(validation) .map(key => paymentInfoValidation[key])
.map(key => validation[key])
.every(({ complete, error }) => complete && !error); .every(({ complete, error }) => complete && !error);
} }
render() { const options = {
const { t } = this.props; style: {
// set color based on theme base: {
style.base.color = this.props.theme === 'night' ? '#fff' : '#0a0a23'; fontSize: '18px',
return ( 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'> <div className='donation-elements'>
<FormGroup> <FormGroup>
<ControlLabel>{t('donate.card-number')}</ControlLabel> <ControlLabel>{t('donate.card-number')}</ControlLabel>
<CardNumberElement <CardNumberElement
className='form-control donate-input-element' className='form-control donate-input-element'
onChange={this.handleInputChange} onChange={handleInputChange}
style={style} options={options}
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@ -91,8 +162,8 @@ class StripeCardForm extends Component {
<Col md={5} xs={12}> <Col md={5} xs={12}>
<CardExpiryElement <CardExpiryElement
className='form-control donate-input-element' className='form-control donate-input-element'
onChange={this.handleInputChange} onChange={handleInputChange}
style={style} options={options}
/> />
</Col> </Col>
<Col className='form-payments-wrapper' md={7} xs={12}> <Col className='form-payments-wrapper' md={7} xs={12}>
@ -108,9 +179,18 @@ class StripeCardForm extends Component {
</Row> </Row>
</FormGroup> </FormGroup>
</div> </div>
); <Button
} block={true}
} bsStyle='primary'
disabled={!stripe}
id='confirm-donation-btn'
type='submit'
>
{getDonationButtonLabel()}
</Button>
</Form>
);
};
StripeCardForm.displayName = 'StripeCardForm'; StripeCardForm.displayName = 'StripeCardForm';
StripeCardForm.propTypes = propTypes; StripeCardForm.propTypes = propTypes;