feat(client): Unify donation loading state (#43179)
* initial loading state unification * feat(client): show buttons after 3 seconds * fix: use window.setInterval explicitly Otherwise TS assumes that it's node's setInterval * feat(client): remove spinner when first button load * feat(client): move the loader to the donate page button area * feat(client): extract grid from modal donation form * feat(client): remove duplicate donation forms * feat(client):extract unused components from donationForm * feat(client): load paypal on load not onInit (for perf) * feat(client): set paypal loading state if stripe already loaded * feat(clinet):make lpaypal oading condition strickt. * Apply suggestions from code review Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * feat: clean up Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -265,7 +265,7 @@ const ShowCertification = (props: IShowCertificationProps): JSX.Element => {
|
|||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
<Row>
|
<Row>
|
||||||
<Col md={8} mdOffset={2} xs={12}>
|
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
<DonateForm
|
<DonateForm
|
||||||
defaultTheme='default'
|
defaultTheme='default'
|
||||||
handleProcessing={handleProcessing}
|
handleProcessing={handleProcessing}
|
||||||
|
@ -24,6 +24,7 @@ function DonateCompletion({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const style =
|
const style =
|
||||||
processing || redirecting ? 'info' : success ? 'success' : 'danger';
|
processing || redirecting ? 'info' : success ? 'success' : 'danger';
|
||||||
|
|
||||||
const heading = redirecting
|
const heading = redirecting
|
||||||
? `${t('donate.redirecting')}`
|
? `${t('donate.redirecting')}`
|
||||||
: processing
|
: processing
|
||||||
@ -31,6 +32,7 @@ function DonateCompletion({
|
|||||||
: success
|
: success
|
||||||
? `${t('donate.thank-you')}`
|
? `${t('donate.thank-you')}`
|
||||||
: `${t('donate.error')}`;
|
: `${t('donate.error')}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert bsStyle={style} className='donation-completion'>
|
<Alert bsStyle={style} className='donation-completion'>
|
||||||
<h4>
|
<h4>
|
||||||
|
@ -2,19 +2,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
import {
|
|
||||||
Col,
|
|
||||||
Row,
|
|
||||||
Tab,
|
|
||||||
Tabs,
|
|
||||||
ToggleButton,
|
|
||||||
ToggleButtonGroup
|
|
||||||
} from '@freecodecamp/react-bootstrap';
|
|
||||||
|
|
||||||
import type { Token } from '@stripe/stripe-js';
|
import type { Token } from '@stripe/stripe-js';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import Spinner from 'react-spinkit';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -48,12 +41,19 @@ const numToCommas = (num: number): string =>
|
|||||||
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||||
|
|
||||||
type DonateFormState = {
|
type DonateFormState = {
|
||||||
donationAmount: number;
|
|
||||||
donationDuration: string;
|
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
redirecting: boolean;
|
redirecting: boolean;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
|
loading: {
|
||||||
|
stripe: boolean;
|
||||||
|
paypal: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type DonateFromComponentState = {
|
||||||
|
donationAmount: number;
|
||||||
|
donationDuration: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DonateFormProps = {
|
type DonateFormProps = {
|
||||||
@ -99,7 +99,7 @@ const mapDispatchToProps = {
|
|||||||
postChargeStripe
|
postChargeStripe
|
||||||
};
|
};
|
||||||
|
|
||||||
class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
class DonateForm extends Component<DonateFormProps, DonateFromComponentState> {
|
||||||
static displayName = 'DonateForm';
|
static displayName = 'DonateForm';
|
||||||
durations: { month: 'monthly'; onetime: 'one-time' };
|
durations: { month: 'monthly'; onetime: 'one-time' };
|
||||||
amounts: { month: number[]; onetime: number[] };
|
amounts: { month: number[]; onetime: number[] };
|
||||||
@ -116,22 +116,16 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
? modalDefaultDonation
|
? modalDefaultDonation
|
||||||
: defaultDonation;
|
: defaultDonation;
|
||||||
|
|
||||||
this.state = {
|
this.state = { ...initialAmountAndDuration };
|
||||||
...initialAmountAndDuration,
|
|
||||||
processing: false,
|
|
||||||
redirecting: false,
|
|
||||||
success: false,
|
|
||||||
error: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
||||||
this.getActiveDonationAmount = this.getActiveDonationAmount.bind(this);
|
this.getActiveDonationAmount = this.getActiveDonationAmount.bind(this);
|
||||||
this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this);
|
this.getDonationButtonLabel = this.getDonationButtonLabel.bind(this);
|
||||||
this.handleSelectAmount = this.handleSelectAmount.bind(this);
|
this.handleSelectAmount = this.handleSelectAmount.bind(this);
|
||||||
this.handleSelectDuration = this.handleSelectDuration.bind(this);
|
this.handleSelectDuration = this.handleSelectDuration.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);
|
this.postStripeDonation = this.postStripeDonation.bind(this);
|
||||||
|
this.handlePaymentButtonLoad = this.handlePaymentButtonLoad.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -141,9 +135,23 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
onDonationStateChange(donationState: AddDonationData) {
|
onDonationStateChange(donationState: AddDonationData) {
|
||||||
// scroll to top
|
// scroll to top
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.props.updateDonationFormState(donationState);
|
this.props.updateDonationFormState({
|
||||||
|
...this.props.donationFormState,
|
||||||
|
...donationState
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePaymentButtonLoad(provider: 'stripe' | 'paypal') {
|
||||||
|
this.props.updateDonationFormState({
|
||||||
|
...this.props.donationFormState,
|
||||||
|
loading: {
|
||||||
|
...this.props.donationFormState.loading,
|
||||||
|
[provider]: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// onload
|
||||||
getActiveDonationAmount(
|
getActiveDonationAmount(
|
||||||
durationSelected: 'month' | 'onetime',
|
durationSelected: 'month' | 'onetime',
|
||||||
amountSelected: number
|
amountSelected: number
|
||||||
@ -213,19 +221,6 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
this.setState({ donationAmount });
|
this.setState({ donationAmount });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAmountButtons(duration: 'month' | 'onetime') {
|
|
||||||
return this.amounts[duration].map((amount: number) => (
|
|
||||||
<ToggleButton
|
|
||||||
className='amount-value'
|
|
||||||
id={`${this.durations[duration]}-donation-${amount}`}
|
|
||||||
key={`${this.durations[duration]}-donation-${amount}`}
|
|
||||||
value={amount}
|
|
||||||
>
|
|
||||||
{this.getFormattedAmountLabel(amount)}
|
|
||||||
</ToggleButton>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDonationDescription() {
|
renderDonationDescription() {
|
||||||
const { donationAmount, donationDuration } = this.state;
|
const { donationAmount, donationDuration } = this.state;
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
@ -243,113 +238,22 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDurationAmountOptions() {
|
|
||||||
const { donationAmount, donationDuration, processing } = this.state;
|
|
||||||
const { t } = this.props;
|
|
||||||
|
|
||||||
return !processing ? (
|
|
||||||
<div>
|
|
||||||
<h3>{t('donate.gift-frequency')}</h3>
|
|
||||||
<Tabs
|
|
||||||
activeKey={donationDuration}
|
|
||||||
animation={false}
|
|
||||||
bsStyle='pills'
|
|
||||||
className='donate-tabs'
|
|
||||||
id='Duration'
|
|
||||||
onSelect={this.handleSelectDuration}
|
|
||||||
>
|
|
||||||
{(Object.keys(this.durations) as ['month' | 'onetime']).map(
|
|
||||||
duration => (
|
|
||||||
<Tab
|
|
||||||
eventKey={duration}
|
|
||||||
key={duration}
|
|
||||||
title={this.durations[duration]}
|
|
||||||
>
|
|
||||||
<Spacer />
|
|
||||||
<h3>{t('donate.gift-amount')}</h3>
|
|
||||||
<div>
|
|
||||||
<ToggleButtonGroup
|
|
||||||
animation={`false`}
|
|
||||||
className='amount-values'
|
|
||||||
name='amounts'
|
|
||||||
onChange={this.handleSelectAmount}
|
|
||||||
type='radio'
|
|
||||||
value={this.getActiveDonationAmount(
|
|
||||||
duration,
|
|
||||||
donationAmount
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{this.renderAmountButtons(duration)}
|
|
||||||
</ToggleButtonGroup>
|
|
||||||
<Spacer />
|
|
||||||
{this.renderDonationDescription()}
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideAmountOptionsCB(hide: boolean) {
|
|
||||||
this.setState({ processing: hide });
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDonationOptions() {
|
|
||||||
const {
|
|
||||||
handleProcessing,
|
|
||||||
isSignedIn,
|
|
||||||
addDonation,
|
|
||||||
t,
|
|
||||||
defaultTheme,
|
|
||||||
theme
|
|
||||||
} = this.props;
|
|
||||||
const { donationAmount, donationDuration } = this.state;
|
|
||||||
const isOneTime = donationDuration === 'onetime';
|
|
||||||
const formlabel = `${t(
|
|
||||||
isOneTime ? 'donate.confirm-2' : 'donate.confirm-3',
|
|
||||||
{ usd: donationAmount / 100 }
|
|
||||||
)}:`;
|
|
||||||
|
|
||||||
const walletlabel = `${t(
|
|
||||||
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
|
||||||
{ usd: donationAmount / 100 }
|
|
||||||
)}:`;
|
|
||||||
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<b>{formlabel}</b>
|
|
||||||
<Spacer />
|
|
||||||
<div className='donate-btn-group'>
|
|
||||||
<WalletsWrapper
|
|
||||||
amount={donationAmount}
|
|
||||||
label={walletlabel}
|
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
|
||||||
postStripeDonation={this.postStripeDonation}
|
|
||||||
refreshErrorMessage={t('donate.refresh-needed')}
|
|
||||||
theme={priorityTheme}
|
|
||||||
/>
|
|
||||||
<PaypalButton
|
|
||||||
addDonation={addDonation}
|
|
||||||
donationAmount={donationAmount}
|
|
||||||
donationDuration={donationDuration}
|
|
||||||
handleProcessing={handleProcessing}
|
|
||||||
isSubscription={isOneTime ? false : true}
|
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
|
||||||
skipAddDonation={!isSignedIn}
|
|
||||||
theme={priorityTheme}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDonation() {
|
resetDonation() {
|
||||||
return this.props.updateDonationFormState({ ...defaultDonationFormState });
|
return this.props.updateDonationFormState({ ...defaultDonationFormState });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paymentButtonsLoader() {
|
||||||
|
return (
|
||||||
|
<div className=' donation-completion donation-completion-loading'>
|
||||||
|
<Spinner
|
||||||
|
className='script-loading-spinner'
|
||||||
|
fadeIn='none'
|
||||||
|
name='line-scale'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderCompletion(props: {
|
renderCompletion(props: {
|
||||||
processing: boolean;
|
processing: boolean;
|
||||||
redirecting: boolean;
|
redirecting: boolean;
|
||||||
@ -360,50 +264,63 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
return <DonateCompletion {...props} />;
|
return <DonateCompletion {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderModalForm() {
|
renderButtonGroup() {
|
||||||
const { donationAmount, donationDuration } = this.state;
|
const { donationAmount, donationDuration } = this.state;
|
||||||
const { handleProcessing, addDonation, defaultTheme, theme, t } =
|
const {
|
||||||
this.props;
|
donationFormState: { loading },
|
||||||
|
handleProcessing,
|
||||||
|
addDonation,
|
||||||
|
defaultTheme,
|
||||||
|
theme,
|
||||||
|
t,
|
||||||
|
isMinimalForm
|
||||||
|
} = this.props;
|
||||||
|
const paymentButtonsLoading = loading.stripe && loading.paypal;
|
||||||
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
||||||
const isOneTime = donationDuration === 'onetime';
|
const isOneTime = donationDuration === 'onetime';
|
||||||
const walletlabel = `${t(
|
const walletlabel = `${t(
|
||||||
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
||||||
{ usd: donationAmount / 100 }
|
{ usd: donationAmount / 100 }
|
||||||
)}:`;
|
)}:`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<>
|
||||||
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
<b className={isMinimalForm ? 'donation-label-modal' : ''}>
|
||||||
<b className='donation-label'>{this.getDonationButtonLabel()}:</b>
|
{this.getDonationButtonLabel()}:
|
||||||
<Spacer />
|
</b>
|
||||||
<div className='donate-btn-group'>
|
<Spacer />
|
||||||
<WalletsWrapper
|
{paymentButtonsLoading && this.paymentButtonsLoader()}
|
||||||
amount={donationAmount}
|
<div className={'donate-btn-group'}>
|
||||||
label={walletlabel}
|
<WalletsWrapper
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
amount={donationAmount}
|
||||||
postStripeDonation={this.postStripeDonation}
|
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
||||||
refreshErrorMessage={t('donate.refresh-needed')}
|
label={walletlabel}
|
||||||
theme={priorityTheme}
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
/>
|
postStripeDonation={this.postStripeDonation}
|
||||||
<PaypalButton
|
refreshErrorMessage={t('donate.refresh-needed')}
|
||||||
addDonation={addDonation}
|
theme={priorityTheme}
|
||||||
donationAmount={donationAmount}
|
/>
|
||||||
donationDuration={donationDuration}
|
<PaypalButton
|
||||||
handleProcessing={handleProcessing}
|
addDonation={addDonation}
|
||||||
onDonationStateChange={this.onDonationStateChange}
|
donationAmount={donationAmount}
|
||||||
theme={defaultTheme ? defaultTheme : theme}
|
donationDuration={donationDuration}
|
||||||
/>
|
handlePaymentButtonLoad={this.handlePaymentButtonLoad}
|
||||||
</div>
|
handleProcessing={handleProcessing}
|
||||||
</Col>
|
isPaypalLoading={loading.paypal}
|
||||||
</Row>
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
|
theme={defaultTheme ? defaultTheme : theme}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPageForm() {
|
renderPageForm() {
|
||||||
return (
|
return (
|
||||||
<Row>
|
<>
|
||||||
<Col xs={12}>{this.renderDonationDescription()}</Col>
|
<div>{this.renderDonationDescription()}</div>
|
||||||
<Col xs={12}>{this.renderDonationOptions()}</Col>
|
<div>{this.renderButtonGroup()}</div>
|
||||||
</Row>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,6 +329,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
donationFormState: { processing, success, error, redirecting },
|
donationFormState: { processing, success, error, redirecting },
|
||||||
isMinimalForm
|
isMinimalForm
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (success || error) {
|
if (success || error) {
|
||||||
return this.renderCompletion({
|
return this.renderCompletion({
|
||||||
processing,
|
processing,
|
||||||
@ -434,7 +352,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
|||||||
reset: this.resetDonation
|
reset: this.resetDonation
|
||||||
})}
|
})}
|
||||||
<div className={processing || redirecting ? 'hide' : ''}>
|
<div className={processing || redirecting ? 'hide' : ''}>
|
||||||
{isMinimalForm ? this.renderModalForm() : this.renderPageForm()}
|
{isMinimalForm ? this.renderButtonGroup() : this.renderPageForm()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -25,6 +25,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.donation-completion-loading {
|
||||||
|
min-height: 154px;
|
||||||
|
}
|
||||||
|
|
||||||
.donation-completion-buttons {
|
.donation-completion-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -245,7 +248,7 @@ li.disabled > a {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
.donation-label,
|
.donation-label-modal,
|
||||||
.donation-modal p,
|
.donation-modal p,
|
||||||
.donation-modal b {
|
.donation-modal b {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -158,7 +158,14 @@ function DonateModal({
|
|||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{recentlyClaimedBlock ? blockDonationText : progressDonationText}
|
{recentlyClaimedBlock ? blockDonationText : progressDonationText}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<DonateForm handleProcessing={handleProcessing} isMinimalForm={true} />
|
<Row>
|
||||||
|
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
<DonateForm
|
||||||
|
handleProcessing={handleProcessing}
|
||||||
|
isMinimalForm={true}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Row>
|
<Row>
|
||||||
<Col sm={4} smOffset={4} xs={8} xsOffset={2}>
|
<Col sm={4} smOffset={4} xs={8} xsOffset={2}>
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import { Loader } from '../../components/helpers';
|
|
||||||
import { scriptLoader, scriptRemover } from '../../utils/script-loaders';
|
import { scriptLoader, scriptRemover } from '../../utils/script-loaders';
|
||||||
|
|
||||||
import type { AddDonationData } from './PaypalButton';
|
import type { AddDonationData } from './PaypalButton';
|
||||||
@ -32,9 +31,15 @@ type PayPalButtonScriptLoaderProps = {
|
|||||||
data: AddDonationData,
|
data: AddDonationData,
|
||||||
actions?: { order: { capture: () => Promise<unknown> } }
|
actions?: { order: { capture: () => Promise<unknown> } }
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
isPaypalLoading: boolean;
|
||||||
onCancel: () => unknown;
|
onCancel: () => unknown;
|
||||||
onError: () => unknown;
|
onError: () => unknown;
|
||||||
style: unknown;
|
onLoad: () => void;
|
||||||
|
style: {
|
||||||
|
color: string;
|
||||||
|
height: number;
|
||||||
|
tagline: boolean;
|
||||||
|
};
|
||||||
planId: string | null;
|
planId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,16 +86,24 @@ export class PayPalButtonScriptLoader extends Component<
|
|||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
if (!window.paypal) {
|
if (!window.paypal) {
|
||||||
this.loadScript(this.props.isSubscription, false);
|
this.loadScript(this.props.isSubscription, false);
|
||||||
|
} else if (this.props.isPaypalLoading) {
|
||||||
|
this.props.onLoad();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: {
|
componentDidUpdate(prevProps: {
|
||||||
isSubscription: boolean;
|
isSubscription: boolean;
|
||||||
style: unknown;
|
style: {
|
||||||
|
color: string;
|
||||||
|
height: number;
|
||||||
|
tagline: boolean;
|
||||||
|
};
|
||||||
}): void {
|
}): void {
|
||||||
if (
|
if (
|
||||||
prevProps.isSubscription !== this.state.isSubscription ||
|
prevProps.isSubscription !== this.state.isSubscription ||
|
||||||
prevProps.style !== this.props.style
|
prevProps.style.color !== this.props.style.color ||
|
||||||
|
prevProps.style.tagline !== this.props.style.tagline ||
|
||||||
|
prevProps.style.height !== this.props.style.height
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
this.setState({ isSdkLoaded: false });
|
this.setState({ isSdkLoaded: false });
|
||||||
@ -114,6 +127,7 @@ export class PayPalButtonScriptLoader extends Component<
|
|||||||
|
|
||||||
onScriptLoad = (): void => {
|
onScriptLoad = (): void => {
|
||||||
this.setState({ isSdkLoaded: true });
|
this.setState({ isSdkLoaded: true });
|
||||||
|
this.props.onLoad();
|
||||||
};
|
};
|
||||||
|
|
||||||
captureOneTimePayment(
|
captureOneTimePayment(
|
||||||
@ -144,7 +158,7 @@ export class PayPalButtonScriptLoader extends Component<
|
|||||||
style
|
style
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!isSdkLoaded) return <Loader />;
|
if (!isSdkLoaded) return <></>;
|
||||||
|
|
||||||
// TODO: fill in the full list of props instead of any
|
// TODO: fill in the full list of props instead of any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -36,10 +36,12 @@ type PaypalButtonProps = {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
isPaypalLoading: boolean;
|
||||||
skipAddDonation?: boolean;
|
skipAddDonation?: boolean;
|
||||||
t: (label: string) => string;
|
t: (label: string) => string;
|
||||||
theme: string;
|
theme: string;
|
||||||
isSubscription?: boolean;
|
isSubscription?: boolean;
|
||||||
|
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PaypalButtonState = {
|
type PaypalButtonState = {
|
||||||
@ -53,6 +55,10 @@ export interface AddDonationData {
|
|||||||
processing: boolean;
|
processing: boolean;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
loading?: {
|
||||||
|
stripe: boolean;
|
||||||
|
paypal: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -120,7 +126,7 @@ export class PaypalButton extends Component<
|
|||||||
|
|
||||||
render(): JSX.Element | null {
|
render(): JSX.Element | null {
|
||||||
const { duration, planId, amount } = this.state;
|
const { duration, planId, amount } = this.state;
|
||||||
const { t, theme } = this.props;
|
const { t, theme, isPaypalLoading } = this.props;
|
||||||
const isSubscription = duration !== 'onetime';
|
const isSubscription = duration !== 'onetime';
|
||||||
const buttonColor = theme === 'night' ? 'white' : 'gold';
|
const buttonColor = theme === 'night' ? 'white' : 'gold';
|
||||||
if (!paypalClientId) {
|
if (!paypalClientId) {
|
||||||
@ -167,6 +173,7 @@ export class PaypalButton extends Component<
|
|||||||
plan_id: planId
|
plan_id: planId
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
isPaypalLoading={isPaypalLoading}
|
||||||
isSubscription={isSubscription}
|
isSubscription={isSubscription}
|
||||||
onApprove={(data: AddDonationData) => {
|
onApprove={(data: AddDonationData) => {
|
||||||
this.handleApproval(data, isSubscription);
|
this.handleApproval(data, isSubscription);
|
||||||
@ -179,14 +186,16 @@ export class PaypalButton extends Component<
|
|||||||
error: t('donate.failed-pay')
|
error: t('donate.failed-pay')
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onError={() =>
|
onError={() => {
|
||||||
|
this.props.handlePaymentButtonLoad('paypal');
|
||||||
this.props.onDonationStateChange({
|
this.props.onDonationStateChange({
|
||||||
redirecting: false,
|
redirecting: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
success: false,
|
success: false,
|
||||||
error: t('donate.try-again')
|
error: t('donate.try-again')
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
|
onLoad={() => this.props.handlePaymentButtonLoad('paypal')}
|
||||||
planId={planId}
|
planId={planId}
|
||||||
style={{
|
style={{
|
||||||
tagline: false,
|
tagline: false,
|
||||||
|
90
client/src/components/Donation/duration-amount-options.tsx
Normal file
90
client/src/components/Donation/duration-amount-options.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup
|
||||||
|
} from '@freecodecamp/react-bootstrap';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Spacer from '../helpers/spacer';
|
||||||
|
|
||||||
|
interface OptionsProps {
|
||||||
|
amounts: { month: []; onetime: [] };
|
||||||
|
month: [];
|
||||||
|
onetime: [];
|
||||||
|
durations: { month: string; onetime: string };
|
||||||
|
getFormattedAmountLabel: (amount: number) => string;
|
||||||
|
getActiveDonationAmount: (
|
||||||
|
duration: 'month' | 'onetime',
|
||||||
|
donationAmount: number
|
||||||
|
) => number;
|
||||||
|
handleSelectDuration: unknown;
|
||||||
|
handleSelectAmount: unknown;
|
||||||
|
donationDuration: 'month' | 'onetime';
|
||||||
|
donationAmount: 500 | 1000;
|
||||||
|
t: (
|
||||||
|
label: string,
|
||||||
|
{ usd, hours }?: { usd?: string | number; hours?: string }
|
||||||
|
) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DurationAmountOptions = ({
|
||||||
|
donationDuration,
|
||||||
|
donationAmount,
|
||||||
|
amounts,
|
||||||
|
durations,
|
||||||
|
getFormattedAmountLabel,
|
||||||
|
getActiveDonationAmount,
|
||||||
|
handleSelectDuration,
|
||||||
|
handleSelectAmount,
|
||||||
|
t
|
||||||
|
}: OptionsProps): JSX.Element => {
|
||||||
|
const renderAmountButtons = (duration: 'month' | 'onetime'): unknown => {
|
||||||
|
return amounts[duration].map((amount: number) => (
|
||||||
|
<ToggleButton
|
||||||
|
className='amount-value'
|
||||||
|
id={`${durations[duration]}-donation-${amount}`}
|
||||||
|
key={`${durations[duration]}-donation-${amount}`}
|
||||||
|
value={amount}
|
||||||
|
>
|
||||||
|
{getFormattedAmountLabel(amount)}
|
||||||
|
</ToggleButton>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
s<h3>{t('donate.gift-frequency')}</h3>
|
||||||
|
<Tabs
|
||||||
|
activeKey={donationDuration}
|
||||||
|
animation={false}
|
||||||
|
bsStyle='pills'
|
||||||
|
className='donate-tabs'
|
||||||
|
id='Duration'
|
||||||
|
onSelect={handleSelectDuration}
|
||||||
|
>
|
||||||
|
{(Object.keys(durations) as ['month' | 'onetime']).map(duration => (
|
||||||
|
<Tab eventKey={duration} key={duration} title={durations[duration]}>
|
||||||
|
<Spacer />
|
||||||
|
<h3>{t('donate.gift-amount')}</h3>
|
||||||
|
<div>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
animation={`false`}
|
||||||
|
className='amount-values'
|
||||||
|
name='amounts'
|
||||||
|
onChange={handleSelectAmount}
|
||||||
|
type='radio'
|
||||||
|
value={getActiveDonationAmount(duration, donationAmount)}
|
||||||
|
>
|
||||||
|
{renderAmountButtons(duration)}
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
<Spacer />
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DurationAmountOptions;
|
@ -22,6 +22,7 @@ interface WrapperProps {
|
|||||||
) => void;
|
) => void;
|
||||||
onDonationStateChange: (donationState: AddDonationData) => void;
|
onDonationStateChange: (donationState: AddDonationData) => void;
|
||||||
refreshErrorMessage: string;
|
refreshErrorMessage: string;
|
||||||
|
handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void;
|
||||||
}
|
}
|
||||||
interface WalletsButtonProps extends WrapperProps {
|
interface WalletsButtonProps extends WrapperProps {
|
||||||
stripe: Stripe | null;
|
stripe: Stripe | null;
|
||||||
@ -34,7 +35,8 @@ const WalletsButton = ({
|
|||||||
theme,
|
theme,
|
||||||
refreshErrorMessage,
|
refreshErrorMessage,
|
||||||
postStripeDonation,
|
postStripeDonation,
|
||||||
onDonationStateChange
|
onDonationStateChange,
|
||||||
|
handlePaymentButtonLoad
|
||||||
}: WalletsButtonProps) => {
|
}: WalletsButtonProps) => {
|
||||||
const [token, setToken] = useState<Token | null>(null);
|
const [token, setToken] = useState<Token | null>(null);
|
||||||
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(
|
||||||
@ -71,7 +73,7 @@ const WalletsButton = ({
|
|||||||
checkpaymentPossiblity(false);
|
checkpaymentPossiblity(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [label, amount, stripe, postStripeDonation]);
|
}, [label, amount, stripe, postStripeDonation, handlePaymentButtonLoad]);
|
||||||
|
|
||||||
const displayRefreshError = (): void => {
|
const displayRefreshError = (): void => {
|
||||||
onDonationStateChange({
|
onDonationStateChange({
|
||||||
@ -87,10 +89,12 @@ const WalletsButton = ({
|
|||||||
{canMakePayment && paymentRequest && (
|
{canMakePayment && paymentRequest && (
|
||||||
<PaymentRequestButtonElement
|
<PaymentRequestButtonElement
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
console.log('click');
|
||||||
if (token) {
|
if (token) {
|
||||||
displayRefreshError();
|
displayRefreshError();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onReady={() => handlePaymentButtonLoad('stripe')}
|
||||||
options={{
|
options={{
|
||||||
style: {
|
style: {
|
||||||
paymentRequestButton: {
|
paymentRequestButton: {
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fcc-loader .sk-spinner {
|
.fcc-loader .sk-spinner,
|
||||||
|
.script-loading-spinner {
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,11 @@ function DonatePage({
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
<DonationText />
|
<DonationText />
|
||||||
<DonateForm handleProcessing={handleProcessing} />
|
<Row>
|
||||||
|
<Col xs={12}>
|
||||||
|
<DonateForm handleProcessing={handleProcessing} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row className='donate-support'>
|
<Row className='donate-support'>
|
||||||
<Col xs={12}>
|
<Col xs={12}>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -33,7 +33,11 @@ export const defaultDonationFormState = {
|
|||||||
redirecting: false,
|
redirecting: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
success: false,
|
success: false,
|
||||||
error: ''
|
error: '',
|
||||||
|
loading: {
|
||||||
|
stripe: true,
|
||||||
|
paypal: true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
Reference in New Issue
Block a user