From 1fec73cdf746f85cfe13ac48afff43e9eb44f068 Mon Sep 17 00:00:00 2001 From: Ahmad Abdolsaheb Date: Thu, 19 Aug 2021 22:47:25 +0300 Subject: [PATCH] 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 * feat: clean up Co-authored-by: Oliver Eyton-Williams --- .../client-only-routes/show-certification.tsx | 2 +- .../components/Donation/DonateCompletion.tsx | 2 + client/src/components/Donation/DonateForm.tsx | 254 ++++++------------ client/src/components/Donation/Donation.css | 5 +- .../src/components/Donation/DonationModal.tsx | 9 +- .../Donation/PayPalButtonScriptLoader.tsx | 24 +- .../src/components/Donation/PaypalButton.tsx | 17 +- .../Donation/duration-amount-options.tsx | 90 +++++++ .../src/components/Donation/walletsButton.tsx | 8 +- client/src/components/helpers/loader.css | 3 +- client/src/pages/donate.tsx | 6 +- client/src/redux/index.js | 6 +- 12 files changed, 241 insertions(+), 185 deletions(-) create mode 100644 client/src/components/Donation/duration-amount-options.tsx diff --git a/client/src/client-only-routes/show-certification.tsx b/client/src/client-only-routes/show-certification.tsx index 3a4b16963b..89c3262c9e 100644 --- a/client/src/client-only-routes/show-certification.tsx +++ b/client/src/client-only-routes/show-certification.tsx @@ -265,7 +265,7 @@ const ShowCertification = (props: IShowCertificationProps): JSX.Element => { )} - +

diff --git a/client/src/components/Donation/DonateForm.tsx b/client/src/components/Donation/DonateForm.tsx index 99374cd156..63f4e8d92d 100644 --- a/client/src/components/Donation/DonateForm.tsx +++ b/client/src/components/Donation/DonateForm.tsx @@ -2,19 +2,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable no-nested-ternary */ -import { - Col, - Row, - Tab, - Tabs, - ToggleButton, - ToggleButtonGroup -} from '@freecodecamp/react-bootstrap'; 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 { @@ -48,12 +41,19 @@ const numToCommas = (num: number): string => num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); type DonateFormState = { - donationAmount: number; - donationDuration: string; processing: boolean; redirecting: boolean; success: boolean; error: string; + loading: { + stripe: boolean; + paypal: boolean; + }; +}; + +type DonateFromComponentState = { + donationAmount: number; + donationDuration: string; }; type DonateFormProps = { @@ -99,7 +99,7 @@ const mapDispatchToProps = { postChargeStripe }; -class DonateForm extends Component { +class DonateForm extends Component { static displayName = 'DonateForm'; durations: { month: 'monthly'; onetime: 'one-time' }; amounts: { month: number[]; onetime: number[] }; @@ -116,22 +116,16 @@ class DonateForm extends Component { ? modalDefaultDonation : defaultDonation; - this.state = { - ...initialAmountAndDuration, - processing: false, - redirecting: false, - success: false, - error: '' - }; + this.state = { ...initialAmountAndDuration }; 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.hideAmountOptionsCB = this.hideAmountOptionsCB.bind(this); this.resetDonation = this.resetDonation.bind(this); this.postStripeDonation = this.postStripeDonation.bind(this); + this.handlePaymentButtonLoad = this.handlePaymentButtonLoad.bind(this); } componentWillUnmount() { @@ -141,9 +135,23 @@ class DonateForm extends Component { onDonationStateChange(donationState: AddDonationData) { // scroll to top 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( durationSelected: 'month' | 'onetime', amountSelected: number @@ -213,19 +221,6 @@ class DonateForm extends Component { this.setState({ donationAmount }); } - renderAmountButtons(duration: 'month' | 'onetime') { - return this.amounts[duration].map((amount: number) => ( - - {this.getFormattedAmountLabel(amount)} - - )); - } - renderDonationDescription() { const { donationAmount, donationDuration } = this.state; const { t } = this.props; @@ -243,113 +238,22 @@ class DonateForm extends Component { ); } - renderDurationAmountOptions() { - const { donationAmount, donationDuration, processing } = this.state; - const { t } = this.props; - - return !processing ? ( -
-

{t('donate.gift-frequency')}

- - {(Object.keys(this.durations) as ['month' | 'onetime']).map( - duration => ( - - -

{t('donate.gift-amount')}

-
- - {this.renderAmountButtons(duration)} - - - {this.renderDonationDescription()} -
-
- ) - )} -
-
- ) : 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 ( -
- {formlabel} - -
- - -
-
- ); - } - resetDonation() { return this.props.updateDonationFormState({ ...defaultDonationFormState }); } + paymentButtonsLoader() { + return ( +
+ +
+ ); + } + renderCompletion(props: { processing: boolean; redirecting: boolean; @@ -360,50 +264,63 @@ class DonateForm extends Component { return ; } - renderModalForm() { + renderButtonGroup() { const { donationAmount, donationDuration } = this.state; - const { handleProcessing, addDonation, defaultTheme, theme, t } = - this.props; + const { + donationFormState: { loading }, + handleProcessing, + addDonation, + defaultTheme, + theme, + t, + isMinimalForm + } = this.props; + const paymentButtonsLoading = loading.stripe && loading.paypal; const priorityTheme = defaultTheme ? defaultTheme : theme; const isOneTime = donationDuration === 'onetime'; const walletlabel = `${t( isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1', { usd: donationAmount / 100 } )}:`; + return ( - - - {this.getDonationButtonLabel()}: - -
- - -
- -
+ <> + + {this.getDonationButtonLabel()}: + + + {paymentButtonsLoading && this.paymentButtonsLoader()} +
+ + +
+ ); } renderPageForm() { return ( - - {this.renderDonationDescription()} - {this.renderDonationOptions()} - + <> +
{this.renderDonationDescription()}
+
{this.renderButtonGroup()}
+ ); } @@ -412,6 +329,7 @@ class DonateForm extends Component { donationFormState: { processing, success, error, redirecting }, isMinimalForm } = this.props; + if (success || error) { return this.renderCompletion({ processing, @@ -434,7 +352,7 @@ class DonateForm extends Component { reset: this.resetDonation })}
- {isMinimalForm ? this.renderModalForm() : this.renderPageForm()} + {isMinimalForm ? this.renderButtonGroup() : this.renderPageForm()}
); diff --git a/client/src/components/Donation/Donation.css b/client/src/components/Donation/Donation.css index 3c5d1ad0d0..508196b7e7 100644 --- a/client/src/components/Donation/Donation.css +++ b/client/src/components/Donation/Donation.css @@ -25,6 +25,9 @@ align-items: center; text-align: center; } +.donation-completion-loading { + min-height: 154px; +} .donation-completion-buttons { display: flex; @@ -245,7 +248,7 @@ li.disabled > a { font-weight: 600; font-size: 1.2rem; } -.donation-label, +.donation-label-modal, .donation-modal p, .donation-modal b { text-align: center; diff --git a/client/src/components/Donation/DonationModal.tsx b/client/src/components/Donation/DonationModal.tsx index bca1161559..81595d98aa 100644 --- a/client/src/components/Donation/DonationModal.tsx +++ b/client/src/components/Donation/DonationModal.tsx @@ -158,7 +158,14 @@ function DonateModal({ {recentlyClaimedBlock ? blockDonationText : progressDonationText} - + + + + + diff --git a/client/src/components/Donation/PayPalButtonScriptLoader.tsx b/client/src/components/Donation/PayPalButtonScriptLoader.tsx index e2812a0e68..7535f25358 100644 --- a/client/src/components/Donation/PayPalButtonScriptLoader.tsx +++ b/client/src/components/Donation/PayPalButtonScriptLoader.tsx @@ -2,7 +2,6 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import { Loader } from '../../components/helpers'; import { scriptLoader, scriptRemover } from '../../utils/script-loaders'; import type { AddDonationData } from './PaypalButton'; @@ -32,9 +31,15 @@ type PayPalButtonScriptLoaderProps = { data: AddDonationData, actions?: { order: { capture: () => Promise } } ) => unknown; + isPaypalLoading: boolean; onCancel: () => unknown; onError: () => unknown; - style: unknown; + onLoad: () => void; + style: { + color: string; + height: number; + tagline: boolean; + }; planId: string | null; }; @@ -81,16 +86,24 @@ export class PayPalButtonScriptLoader extends Component< componentDidMount(): void { if (!window.paypal) { this.loadScript(this.props.isSubscription, false); + } else if (this.props.isPaypalLoading) { + this.props.onLoad(); } } componentDidUpdate(prevProps: { isSubscription: boolean; - style: unknown; + style: { + color: string; + height: number; + tagline: boolean; + }; }): void { if ( 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 this.setState({ isSdkLoaded: false }); @@ -114,6 +127,7 @@ export class PayPalButtonScriptLoader extends Component< onScriptLoad = (): void => { this.setState({ isSdkLoaded: true }); + this.props.onLoad(); }; captureOneTimePayment( @@ -144,7 +158,7 @@ export class PayPalButtonScriptLoader extends Component< style } = this.props; - if (!isSdkLoaded) return ; + if (!isSdkLoaded) return <>; // TODO: fill in the full list of props instead of any // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/client/src/components/Donation/PaypalButton.tsx b/client/src/components/Donation/PaypalButton.tsx index 0e13988d36..39bb89a9bb 100644 --- a/client/src/components/Donation/PaypalButton.tsx +++ b/client/src/components/Donation/PaypalButton.tsx @@ -36,10 +36,12 @@ type PaypalButtonProps = { success: boolean; error: string | null; }) => void; + isPaypalLoading: boolean; skipAddDonation?: boolean; t: (label: string) => string; theme: string; isSubscription?: boolean; + handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void; }; type PaypalButtonState = { @@ -53,6 +55,10 @@ export interface AddDonationData { processing: boolean; success: boolean; error: string | null; + loading?: { + stripe: boolean; + paypal: boolean; + }; } const { @@ -120,7 +126,7 @@ export class PaypalButton extends Component< render(): JSX.Element | null { const { duration, planId, amount } = this.state; - const { t, theme } = this.props; + const { t, theme, isPaypalLoading } = this.props; const isSubscription = duration !== 'onetime'; const buttonColor = theme === 'night' ? 'white' : 'gold'; if (!paypalClientId) { @@ -167,6 +173,7 @@ export class PaypalButton extends Component< plan_id: planId }); }} + isPaypalLoading={isPaypalLoading} isSubscription={isSubscription} onApprove={(data: AddDonationData) => { this.handleApproval(data, isSubscription); @@ -179,14 +186,16 @@ export class PaypalButton extends Component< error: t('donate.failed-pay') }); }} - onError={() => + onError={() => { + this.props.handlePaymentButtonLoad('paypal'); this.props.onDonationStateChange({ redirecting: false, processing: false, success: false, error: t('donate.try-again') - }) - } + }); + }} + onLoad={() => this.props.handlePaymentButtonLoad('paypal')} planId={planId} style={{ tagline: false, diff --git a/client/src/components/Donation/duration-amount-options.tsx b/client/src/components/Donation/duration-amount-options.tsx new file mode 100644 index 0000000000..9251612887 --- /dev/null +++ b/client/src/components/Donation/duration-amount-options.tsx @@ -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) => ( + + {getFormattedAmountLabel(amount)} + + )); + }; + + return ( +
+ s

{t('donate.gift-frequency')}

+ + {(Object.keys(durations) as ['month' | 'onetime']).map(duration => ( + + +

{t('donate.gift-amount')}

+
+ + {renderAmountButtons(duration)} + + +
+
+ ))} +
+
+ ); +}; + +export default DurationAmountOptions; diff --git a/client/src/components/Donation/walletsButton.tsx b/client/src/components/Donation/walletsButton.tsx index 87c70ff825..729ca2ab30 100644 --- a/client/src/components/Donation/walletsButton.tsx +++ b/client/src/components/Donation/walletsButton.tsx @@ -22,6 +22,7 @@ interface WrapperProps { ) => void; onDonationStateChange: (donationState: AddDonationData) => void; refreshErrorMessage: string; + handlePaymentButtonLoad: (provider: 'stripe' | 'paypal') => void; } interface WalletsButtonProps extends WrapperProps { stripe: Stripe | null; @@ -34,7 +35,8 @@ const WalletsButton = ({ theme, refreshErrorMessage, postStripeDonation, - onDonationStateChange + onDonationStateChange, + handlePaymentButtonLoad }: WalletsButtonProps) => { const [token, setToken] = useState(null); const [paymentRequest, setPaymentRequest] = useState( @@ -71,7 +73,7 @@ const WalletsButton = ({ checkpaymentPossiblity(false); } }); - }, [label, amount, stripe, postStripeDonation]); + }, [label, amount, stripe, postStripeDonation, handlePaymentButtonLoad]); const displayRefreshError = (): void => { onDonationStateChange({ @@ -87,10 +89,12 @@ const WalletsButton = ({ {canMakePayment && paymentRequest && ( { + console.log('click'); if (token) { displayRefreshError(); } }} + onReady={() => handlePaymentButtonLoad('stripe')} options={{ style: { paymentRequestButton: { diff --git a/client/src/components/helpers/loader.css b/client/src/components/helpers/loader.css index 9b37eb0134..5acf705bd8 100644 --- a/client/src/components/helpers/loader.css +++ b/client/src/components/helpers/loader.css @@ -6,7 +6,8 @@ align-items: center; } -.fcc-loader .sk-spinner { +.fcc-loader .sk-spinner, +.script-loading-spinner { color: var(--secondary-color); } diff --git a/client/src/pages/donate.tsx b/client/src/pages/donate.tsx index 44466c8264..ec2ea9ed59 100644 --- a/client/src/pages/donate.tsx +++ b/client/src/pages/donate.tsx @@ -113,7 +113,11 @@ function DonatePage({ ) : null} - + + + + +
diff --git a/client/src/redux/index.js b/client/src/redux/index.js index f9424c17a1..6664db715d 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -33,7 +33,11 @@ export const defaultDonationFormState = { redirecting: false, processing: false, success: false, - error: '' + error: '', + loading: { + stripe: true, + paypal: true + } }; const initialState = {