mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
Co-authored-by: Ahmad Abdolsaheb <ahmad.abdolsaheb@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Sem Bauke <semboot699@gmail.com>
355 lines
9.6 KiB
TypeScript
355 lines
9.6 KiB
TypeScript
/* eslint-disable @typescript-eslint/unbound-method */
|
|
import type { Token } from '@stripe/stripe-js';
|
|
import React, { Component } from 'react';
|
|
import { withTranslation } from 'react-i18next';
|
|
import { connect } from 'react-redux';
|
|
import Spinner from 'react-spinkit';
|
|
import { createSelector } from 'reselect';
|
|
import type { TFunction } from 'i18next';
|
|
import { Spacer } from '@freecodecamp/ui';
|
|
|
|
import {
|
|
defaultDonation,
|
|
DonationAmount,
|
|
type DonationConfig
|
|
} from '../../../../shared/config/donation-settings';
|
|
import { defaultDonationFormState } from '../../redux';
|
|
import { updateDonationFormState, postCharge } from '../../redux/actions';
|
|
import {
|
|
isSignedInSelector,
|
|
userSelector,
|
|
isDonatingSelector,
|
|
signInLoadingSelector,
|
|
donationFormStateSelector,
|
|
completedChallengesSelector,
|
|
themeSelector
|
|
} from '../../redux/selectors';
|
|
import { LocalStorageThemes, DonateFormState } from '../../redux/types';
|
|
import type { CompletedChallenge } from '../../redux/prop-types';
|
|
import { CENTS_IN_DOLLAR, formattedAmountLabel } from './utils';
|
|
import DonateCompletion from './donate-completion';
|
|
import PatreonButton from './patreon-button';
|
|
import PaypalButton from './paypal-button';
|
|
import StripeCardForm from './stripe-card-form';
|
|
import WalletsWrapper from './wallets-button';
|
|
import SecurityLockIcon from './security-lock-icon';
|
|
import {
|
|
PaymentProvider,
|
|
PaymentContext,
|
|
PostPayment,
|
|
HandleAuthentication,
|
|
DonationApprovalData
|
|
} from './types';
|
|
|
|
import './donation.css';
|
|
|
|
type DonateFormComponentState = DonationConfig;
|
|
|
|
type PostCharge = (data: {
|
|
paymentProvider: PaymentProvider;
|
|
paymentContext: PaymentContext;
|
|
amount: number;
|
|
duration: string;
|
|
data?: DonationApprovalData;
|
|
token?: Token;
|
|
email?: string;
|
|
name?: string | undefined;
|
|
paymentMethodId?: string;
|
|
handleAuthentication?: HandleAuthentication;
|
|
subscriptionId?: string;
|
|
}) => void;
|
|
|
|
type DonateFormProps = {
|
|
postCharge: PostCharge;
|
|
defaultTheme?: LocalStorageThemes;
|
|
email: string;
|
|
handleProcessing?: () => void;
|
|
editAmount?: () => void;
|
|
selectedDonationAmount?: DonationAmount;
|
|
donationFormState: DonateFormState;
|
|
isMinimalForm?: boolean;
|
|
isSignedIn: boolean;
|
|
isDonating: boolean;
|
|
showLoading: boolean;
|
|
t: TFunction;
|
|
updateDonationFormState: (state: DonationApprovalData) => unknown;
|
|
paymentContext: PaymentContext;
|
|
completedChallenges: CompletedChallenge[];
|
|
theme: LocalStorageThemes;
|
|
};
|
|
|
|
const mapStateToProps = createSelector(
|
|
signInLoadingSelector,
|
|
isSignedInSelector,
|
|
isDonatingSelector,
|
|
donationFormStateSelector,
|
|
userSelector,
|
|
completedChallengesSelector,
|
|
themeSelector,
|
|
(
|
|
showLoading: DonateFormProps['showLoading'],
|
|
isSignedIn: DonateFormProps['isSignedIn'],
|
|
isDonating: DonateFormProps['isDonating'],
|
|
donationFormState: DonateFormState,
|
|
{ email }: { email: string },
|
|
completedChallenges: CompletedChallenge[],
|
|
theme: LocalStorageThemes
|
|
) => ({
|
|
isSignedIn,
|
|
isDonating,
|
|
showLoading,
|
|
donationFormState,
|
|
email,
|
|
completedChallenges,
|
|
theme
|
|
})
|
|
);
|
|
|
|
const mapDispatchToProps = {
|
|
postCharge,
|
|
updateDonationFormState
|
|
};
|
|
|
|
const PaymentButtonsLoader = () => {
|
|
return (
|
|
<div className=' donation-completion donation-completion-loading'>
|
|
<Spinner
|
|
className='script-loading-spinner'
|
|
fadeIn='none'
|
|
name='line-scale'
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|
static displayName = 'DonateForm';
|
|
constructor(props: DonateFormProps) {
|
|
super(props);
|
|
|
|
const initialAmountAndDuration: DonationConfig = defaultDonation;
|
|
|
|
this.state = { ...initialAmountAndDuration };
|
|
|
|
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
|
this.resetDonation = this.resetDonation.bind(this);
|
|
this.postPayment = this.postPayment.bind(this);
|
|
this.handlePaymentButtonLoad = this.handlePaymentButtonLoad.bind(this);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.resetDonation();
|
|
}
|
|
|
|
onDonationStateChange(donationState: DonationApprovalData) {
|
|
// scroll to top
|
|
window.scrollTo(0, 0);
|
|
this.props.updateDonationFormState({
|
|
...this.props.donationFormState,
|
|
...donationState
|
|
});
|
|
}
|
|
|
|
handlePaymentButtonLoad(provider: 'stripe' | 'paypal') {
|
|
this.props.updateDonationFormState({
|
|
...this.props.donationFormState,
|
|
loading: {
|
|
...this.props.donationFormState.loading,
|
|
[provider]: false
|
|
}
|
|
});
|
|
}
|
|
|
|
postPayment = ({
|
|
paymentProvider,
|
|
data,
|
|
payerEmail,
|
|
payerName,
|
|
paymentMethodId,
|
|
handleAuthentication,
|
|
subscriptionId
|
|
}: PostPayment): void => {
|
|
const { donationAmount, donationDuration: duration } = this.state;
|
|
const { paymentContext, email, selectedDonationAmount } = this.props;
|
|
const amount = selectedDonationAmount || donationAmount;
|
|
|
|
this.props.postCharge({
|
|
paymentProvider,
|
|
paymentContext,
|
|
amount,
|
|
duration,
|
|
data,
|
|
email: email || payerEmail,
|
|
name: payerName,
|
|
paymentMethodId,
|
|
handleAuthentication,
|
|
subscriptionId
|
|
});
|
|
if (this.props.handleProcessing) this.props.handleProcessing();
|
|
};
|
|
|
|
resetDonation() {
|
|
return this.props.updateDonationFormState({ ...defaultDonationFormState });
|
|
}
|
|
|
|
renderButtonGroup() {
|
|
const { donationAmount: defaultAmount, donationDuration } = this.state;
|
|
const {
|
|
donationFormState: { loading, processing },
|
|
defaultTheme,
|
|
theme,
|
|
t,
|
|
isMinimalForm,
|
|
isSignedIn,
|
|
isDonating,
|
|
editAmount,
|
|
selectedDonationAmount,
|
|
completedChallenges
|
|
} = this.props;
|
|
const donationAmount: DonationAmount =
|
|
selectedDonationAmount || defaultAmount;
|
|
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
|
const walletlabel = `${t('donate.wallet-label-1', {
|
|
usd: donationAmount / CENTS_IN_DOLLAR
|
|
})}:`;
|
|
|
|
const threeChallengesCompleted = completedChallenges.length >= 3;
|
|
|
|
const showMinimalPayments =
|
|
isSignedIn && (isMinimalForm || !isDonating) && threeChallengesCompleted;
|
|
|
|
const confirmationWithEditAmount = (
|
|
<>
|
|
<b>
|
|
{t('donate.confirm-multitier', {
|
|
usd: formattedAmountLabel(donationAmount)
|
|
})}
|
|
</b>
|
|
|
|
<button
|
|
type='button'
|
|
className='edit-amount-button'
|
|
onClick={editAmount}
|
|
>
|
|
{t('donate.edit-amount')}
|
|
</button>
|
|
</>
|
|
);
|
|
|
|
const confirmationClass = () => {
|
|
if (editAmount) return 'edit-amount-confirmation';
|
|
if (isMinimalForm) return 'donation-label-modal';
|
|
return '';
|
|
};
|
|
return (
|
|
<>
|
|
<div className={confirmationClass()}>{confirmationWithEditAmount}</div>
|
|
<Spacer size={editAmount ? 'xs' : 'm'} />
|
|
<fieldset
|
|
data-playwright-test-label='donation-form'
|
|
className={'donate-btn-group security-legend'}
|
|
>
|
|
<legend>
|
|
<SecurityLockIcon />
|
|
{t('donate.secure-donation')}
|
|
</legend>
|
|
|
|
{loading.stripe && loading.paypal && <PaymentButtonsLoader />}
|
|
<WalletsWrapper
|
|
amount={donationAmount}
|
|
duration={donationDuration}
|
|
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
|
label={walletlabel}
|
|
onDonationStateChange={this.onDonationStateChange}
|
|
postPayment={this.postPayment}
|
|
theme={priorityTheme}
|
|
/>
|
|
<PaypalButton
|
|
donationAmount={donationAmount}
|
|
donationDuration={donationDuration}
|
|
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
|
postPayment={this.postPayment}
|
|
isMinimalForm={showMinimalPayments}
|
|
isPaypalLoading={loading.paypal}
|
|
onDonationStateChange={this.onDonationStateChange}
|
|
theme={priorityTheme}
|
|
/>
|
|
{(!loading.stripe || !loading.paypal) && (
|
|
<PatreonButton
|
|
postPayment={this.postPayment}
|
|
donationAmount={donationAmount}
|
|
/>
|
|
)}
|
|
{showMinimalPayments && (
|
|
<>
|
|
<div className='separator'>{t('donate.or-card')}</div>
|
|
<StripeCardForm
|
|
onDonationStateChange={this.onDonationStateChange}
|
|
postPayment={this.postPayment}
|
|
processing={processing}
|
|
t={t}
|
|
theme={priorityTheme}
|
|
/>
|
|
</>
|
|
)}
|
|
</fieldset>
|
|
</>
|
|
);
|
|
}
|
|
|
|
renderPageForm() {
|
|
return (
|
|
<>
|
|
<div>{this.renderButtonGroup()}</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
donationFormState: { processing, success, error, redirecting },
|
|
isMinimalForm,
|
|
isSignedIn
|
|
} = this.props;
|
|
|
|
if (success || error) {
|
|
return (
|
|
<DonateCompletion
|
|
processing={processing}
|
|
redirecting={redirecting}
|
|
success={success}
|
|
error={error}
|
|
isSignedIn={isSignedIn}
|
|
reset={this.resetDonation}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// keep payment provider elements on DOM during processing and redirect to avoid errors.
|
|
return (
|
|
<>
|
|
{(processing || redirecting) && (
|
|
<DonateCompletion
|
|
processing={processing}
|
|
redirecting={redirecting}
|
|
success={success}
|
|
error={error}
|
|
isSignedIn={isSignedIn}
|
|
reset={this.resetDonation}
|
|
/>
|
|
)}
|
|
<div className={processing || redirecting ? 'hide' : ''}>
|
|
{isMinimalForm ? this.renderButtonGroup() : this.renderPageForm()}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
DonateForm.displayName = 'DonateForm';
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(withTranslation()(DonateForm));
|