Files
freeCodeCamp/client/src/components/Donation/PaypalButton.tsx
Ahmad Abdolsaheb 1fec73cdf7 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>
2021-08-19 14:47:25 -05:00

223 lines
6.1 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable camelcase */
import React, { Component } from 'react';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
paypalConfigurator,
paypalConfigTypes,
defaultDonation
} from '../../../../config/donation-settings';
import envData from '../../../../config/env.json';
import { signInLoadingSelector, userSelector } from '../../redux';
import PayPalButtonScriptLoader from './PayPalButtonScriptLoader';
type PaypalButtonProps = {
addDonation: (data: AddDonationData) => void;
donationAmount: number;
donationDuration: string;
handleProcessing: (
duration: string,
amount: number,
action: string
) => unknown;
isDonating: boolean;
onDonationStateChange: ({
redirecting,
processing,
success,
error
}: {
redirecting: boolean;
processing: boolean;
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 = {
amount: number;
duration: string;
planId: string | null;
};
export interface AddDonationData {
redirecting: boolean;
processing: boolean;
success: boolean;
error: string | null;
loading?: {
stripe: boolean;
paypal: boolean;
};
}
const {
paypalClientId,
deploymentEnv
}: { paypalClientId: string | null; deploymentEnv: 'staging' | 'live' } =
envData as {
paypalClientId: string | null;
deploymentEnv: 'staging' | 'live';
};
export class PaypalButton extends Component<
PaypalButtonProps,
PaypalButtonState
> {
static displayName = 'PaypalButton';
state: PaypalButtonState = {
amount: defaultDonation.donationAmount,
duration: defaultDonation.donationDuration,
planId: null
};
constructor(props: PaypalButtonProps) {
super(props);
this.handleApproval = this.handleApproval.bind(this);
}
static getDerivedStateFromProps(props: PaypalButtonProps): PaypalButtonState {
const { donationAmount, donationDuration } = props;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const configurationObj: {
amount: number;
duration: string;
planId: string | null;
} = paypalConfigurator(
donationAmount,
donationDuration,
paypalConfigTypes[deploymentEnv || 'staging']
);
// re-implement it as a deep comparison.
// if (state === configurationObj) {
// return null;
// }
return { ...configurationObj };
}
handleApproval = (data: AddDonationData, isSubscription: boolean): void => {
const { amount, duration } = this.state;
const { skipAddDonation = false } = this.props;
// Skip the api if user is not signed in or if its a one-time donation
if (!skipAddDonation || isSubscription) {
this.props.addDonation(data);
}
this.props.handleProcessing(duration, amount, 'Paypal payment submission');
// Show success anytime because the payment has gone through paypal
this.props.onDonationStateChange({
redirecting: false,
processing: false,
success: true,
error: data.error ? data.error : null
});
};
render(): JSX.Element | null {
const { duration, planId, amount } = this.state;
const { t, theme, isPaypalLoading } = this.props;
const isSubscription = duration !== 'onetime';
const buttonColor = theme === 'night' ? 'white' : 'gold';
if (!paypalClientId) {
return null;
}
return (
<div className={'paypal-buttons-container'}>
{/* help needed */}
<PayPalButtonScriptLoader
clientId={paypalClientId}
createOrder={(
data: unknown,
actions: {
order: {
create: (arg0: {
purchase_units: {
amount: { currency_code: string; value: string };
}[];
}) => unknown;
};
}
) => {
return actions.order.create({
purchase_units: [
{
amount: {
currency_code: 'USD',
value: (amount / 100).toString()
}
}
]
});
}}
createSubscription={(
data: unknown,
actions: {
subscription: {
create: (arg0: { plan_id: string | null }) => unknown;
};
}
) => {
return actions.subscription.create({
plan_id: planId
});
}}
isPaypalLoading={isPaypalLoading}
isSubscription={isSubscription}
onApprove={(data: AddDonationData) => {
this.handleApproval(data, isSubscription);
}}
onCancel={() => {
this.props.onDonationStateChange({
redirecting: false,
processing: false,
success: false,
error: t('donate.failed-pay')
});
}}
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,
height: 43,
color: buttonColor
}}
/>
</div>
);
}
}
const mapStateToProps = createSelector(
userSelector,
signInLoadingSelector,
({ isDonating }: { isDonating: boolean }, showLoading: boolean) => ({
isDonating,
showLoading
})
);
PaypalButton.displayName = 'PaypalButton';
export default connect(mapStateToProps)(withTranslation()(PaypalButton));