feat(client): migrate donate module to ts (#42561)
* change DonationTextComponent extension to tsx * migrate DonationTextComponents to ts * change DonationModal extension to tsx * add @types/react-redux * migrate DonationModal to ts * change PaypalButton extension to ts * change DonateCompletion extension to tsx * migrate DonateCompletion to TypeScript * change PayPalButtonLoader extension to tsx * first changes in paypal button (help needed) * first changes in PayPalButtonScriptLoader (help needed) * change DonateForm extension to tsx * migrate donate module to ts * Update client/src/components/Donation/DonateForm.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/Donation/DonationModal.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * Update client/src/components/Donation/DonationModal.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * Update client/src/components/Donation/DonateForm.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/Donation/DonateForm.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Delete console.log client/src/components/Donation/DonationModal.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * applied changes requested * fix: readjust default one time amount * fix types Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * chore: restore comments.json * fix: type assertion * fix: specific DonateForm props * Apply suggestions from code review Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/Donation/PaypalButton.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix:set default stat for paypalbutton Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> Co-authored-by: Ahmad Abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
35
client/package-lock.json
generated
35
client/package-lock.json
generated
@ -4375,6 +4375,14 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-scrollable-anchor": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-scrollable-anchor/-/react-scrollable-anchor-0.6.1.tgz",
|
||||
"integrity": "sha512-ExstDPDHD0oC8WxHErnZhtdaCmGncBxRu24yRk8BNym7tCoR9BEpDACVp2SWPMttIAfUTv1/5yk3hsIL5R5VCQ==",
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-spinkit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-spinkit/-/react-spinkit-3.0.7.tgz",
|
||||
@ -5733,6 +5741,15 @@
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
||||
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
@ -9624,6 +9641,12 @@
|
||||
"token-types": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"optional": true
|
||||
},
|
||||
"filesize": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz",
|
||||
@ -15259,6 +15282,12 @@
|
||||
"resolved": "https://registry.npmjs.org/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz",
|
||||
"integrity": "sha1-Cr+2rYNXGLn7Te8GdOBmV6lUN1w="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"optional": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.1.23",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
|
||||
@ -21183,7 +21212,11 @@
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "3.1.0",
|
||||
|
@ -53,6 +53,7 @@
|
||||
"@freecodecamp/strip-comments": "3.0.1",
|
||||
"@loadable/component": "5.15.0",
|
||||
"@reach/router": "1.3.4",
|
||||
"@types/react-scrollable-anchor": "^0.6.1",
|
||||
"algoliasearch": "4.10.3",
|
||||
"assert": "2.0.0",
|
||||
"babel-plugin-preval": "5.0.0",
|
||||
|
@ -1,17 +1,16 @@
|
||||
import { Alert, Button } from '@freecodecamp/react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Spinner from 'react-spinkit';
|
||||
|
||||
import './Donation.css';
|
||||
|
||||
const propTypes = {
|
||||
error: PropTypes.string,
|
||||
processing: PropTypes.bool,
|
||||
redirecting: PropTypes.bool,
|
||||
reset: PropTypes.func.isRequired,
|
||||
success: PropTypes.bool
|
||||
type DonateCompletionProps = {
|
||||
error: string | null;
|
||||
processing: boolean;
|
||||
redirecting: boolean;
|
||||
reset: () => unknown;
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
function DonateCompletion({
|
||||
@ -20,7 +19,7 @@ function DonateCompletion({
|
||||
success,
|
||||
redirecting,
|
||||
error = null
|
||||
}) {
|
||||
}: DonateCompletionProps): JSX.Element {
|
||||
/* eslint-disable no-nested-ternary */
|
||||
const { t } = useTranslation();
|
||||
const style =
|
||||
@ -68,6 +67,5 @@ function DonateCompletion({
|
||||
}
|
||||
|
||||
DonateCompletion.displayName = 'DonateCompletion';
|
||||
DonateCompletion.propTypes = propTypes;
|
||||
|
||||
export default DonateCompletion;
|
@ -1,3 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import {
|
||||
Col,
|
||||
@ -7,7 +11,6 @@ import {
|
||||
ToggleButton,
|
||||
ToggleButtonGroup
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
@ -30,27 +33,41 @@ import {
|
||||
userSelector
|
||||
} from '../../redux';
|
||||
import Spacer from '../helpers/spacer';
|
||||
|
||||
import DonateCompletion from './DonateCompletion';
|
||||
|
||||
import type { AddDonationData } from './PaypalButton';
|
||||
import PaypalButton from './PaypalButton';
|
||||
|
||||
import './Donation.css';
|
||||
|
||||
const numToCommas = num =>
|
||||
const numToCommas = (num: number): string =>
|
||||
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
|
||||
const propTypes = {
|
||||
addDonation: PropTypes.func,
|
||||
defaultTheme: PropTypes.string,
|
||||
donationFormState: PropTypes.object,
|
||||
email: PropTypes.string,
|
||||
handleProcessing: PropTypes.func,
|
||||
isDonating: PropTypes.bool,
|
||||
isMinimalForm: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool,
|
||||
showLoading: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
theme: PropTypes.string,
|
||||
updateDonationFormState: PropTypes.func
|
||||
type DonateFormState = {
|
||||
donationAmount: number;
|
||||
donationDuration: string;
|
||||
processing: boolean;
|
||||
redirecting: boolean;
|
||||
success: boolean;
|
||||
error: string;
|
||||
};
|
||||
|
||||
type DonateFormProps = {
|
||||
addDonation: (data: unknown) => unknown;
|
||||
defaultTheme?: string;
|
||||
email: string;
|
||||
handleProcessing: (duration: string, amount: number, action: string) => void;
|
||||
donationFormState: DonateFormState;
|
||||
isMinimalForm?: boolean;
|
||||
isSignedIn: boolean;
|
||||
showLoading: boolean;
|
||||
t: (
|
||||
label: string,
|
||||
{ usd, hours }?: { usd?: string | number; hours?: string }
|
||||
) => string;
|
||||
theme: string;
|
||||
updateDonationFormState: (state: AddDonationData) => unknown;
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
@ -58,7 +75,12 @@ const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
donationFormStateSelector,
|
||||
userSelector,
|
||||
(showLoading, isSignedIn, donationFormState, { email, theme }) => ({
|
||||
(
|
||||
showLoading: DonateFormProps['showLoading'],
|
||||
isSignedIn: DonateFormProps['isSignedIn'],
|
||||
donationFormState: DonateFormState,
|
||||
{ email, theme }: { email: string; theme: string }
|
||||
) => ({
|
||||
isSignedIn,
|
||||
showLoading,
|
||||
donationFormState,
|
||||
@ -72,11 +94,17 @@ const mapDispatchToProps = {
|
||||
updateDonationFormState
|
||||
};
|
||||
|
||||
class DonateForm extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
class DonateForm extends Component<DonateFormProps, DonateFormState> {
|
||||
static displayName = 'DonateForm';
|
||||
durations: { month: 'monthly'; onetime: 'one-time' };
|
||||
amounts: { month: number[]; onetime: number[] };
|
||||
constructor(props: DonateFormProps) {
|
||||
super(props);
|
||||
|
||||
this.durations = durationsConfig;
|
||||
this.durations = durationsConfig as {
|
||||
month: 'monthly';
|
||||
onetime: 'one-time';
|
||||
};
|
||||
this.amounts = amountsConfig;
|
||||
|
||||
const initialAmountAndDuration = this.props.isMinimalForm
|
||||
@ -85,7 +113,10 @@ class DonateForm extends Component {
|
||||
|
||||
this.state = {
|
||||
...initialAmountAndDuration,
|
||||
processing: false
|
||||
processing: false,
|
||||
redirecting: false,
|
||||
success: false,
|
||||
error: ''
|
||||
};
|
||||
|
||||
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
||||
@ -101,23 +132,26 @@ class DonateForm extends Component {
|
||||
this.resetDonation();
|
||||
}
|
||||
|
||||
onDonationStateChange(donationState) {
|
||||
onDonationStateChange(donationState: AddDonationData) {
|
||||
// scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
this.props.updateDonationFormState(donationState);
|
||||
}
|
||||
|
||||
getActiveDonationAmount(durationSelected, amountSelected) {
|
||||
getActiveDonationAmount(
|
||||
durationSelected: 'month' | 'onetime',
|
||||
amountSelected: number
|
||||
): number {
|
||||
return this.amounts[durationSelected].includes(amountSelected)
|
||||
? amountSelected
|
||||
: defaultAmount[durationSelected] || this.amounts[durationSelected][0];
|
||||
}
|
||||
|
||||
convertToTimeContributed(amount) {
|
||||
convertToTimeContributed(amount: number) {
|
||||
return numToCommas((amount / 100) * 50);
|
||||
}
|
||||
|
||||
getFormattedAmountLabel(amount) {
|
||||
getFormattedAmountLabel(amount: number): string {
|
||||
return `${numToCommas(amount / 100)}`;
|
||||
}
|
||||
|
||||
@ -141,17 +175,17 @@ class DonateForm extends Component {
|
||||
return donationBtnLabel;
|
||||
}
|
||||
|
||||
handleSelectDuration(donationDuration) {
|
||||
handleSelectDuration(donationDuration: 'month' | 'onetime') {
|
||||
const donationAmount = this.getActiveDonationAmount(donationDuration, 0);
|
||||
this.setState({ donationDuration, donationAmount });
|
||||
}
|
||||
|
||||
handleSelectAmount(donationAmount) {
|
||||
handleSelectAmount(donationAmount: number) {
|
||||
this.setState({ donationAmount });
|
||||
}
|
||||
|
||||
renderAmountButtons(duration) {
|
||||
return this.amounts[duration].map(amount => (
|
||||
renderAmountButtons(duration: 'month' | 'onetime') {
|
||||
return this.amounts[duration].map((amount: number) => (
|
||||
<ToggleButton
|
||||
className='amount-value'
|
||||
id={`${this.durations[duration]}-donation-${amount}`}
|
||||
@ -195,36 +229,41 @@ class DonateForm extends Component {
|
||||
id='Duration'
|
||||
onSelect={this.handleSelectDuration}
|
||||
>
|
||||
{Object.keys(this.durations).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>
|
||||
{(Object.keys(this.durations) as ['month' | 'onetime']).map(
|
||||
duration => (
|
||||
<Tab
|
||||
eventKey={duration}
|
||||
key={duration}
|
||||
title={this.durations[duration]}
|
||||
>
|
||||
<Spacer />
|
||||
{this.renderDonationDescription()}
|
||||
</div>
|
||||
</Tab>
|
||||
))}
|
||||
<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) {
|
||||
hideAmountOptionsCB(hide: boolean) {
|
||||
this.setState({ processing: hide });
|
||||
}
|
||||
|
||||
@ -271,7 +310,13 @@ class DonateForm extends Component {
|
||||
return this.props.updateDonationFormState({ ...defaultDonationFormState });
|
||||
}
|
||||
|
||||
renderCompletion(props) {
|
||||
renderCompletion(props: {
|
||||
processing: boolean;
|
||||
redirecting: boolean;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
reset: () => unknown;
|
||||
}) {
|
||||
return <DonateCompletion {...props} />;
|
||||
}
|
||||
|
||||
@ -333,9 +378,7 @@ class DonateForm extends Component {
|
||||
reset: this.resetDonation
|
||||
})}
|
||||
<div className={processing || redirecting ? 'hide' : ''}>
|
||||
{isMinimalForm
|
||||
? this.renderModalForm(processing)
|
||||
: this.renderPageForm(processing)}
|
||||
{isMinimalForm ? this.renderModalForm() : this.renderPageForm()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@ -343,7 +386,6 @@ class DonateForm extends Component {
|
||||
}
|
||||
|
||||
DonateForm.displayName = 'DonateForm';
|
||||
DonateForm.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
@ -1,11 +1,10 @@
|
||||
/* eslint-disable max-len */
|
||||
import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import { WindowLocation } from '@reach/router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { goToAnchor } from 'react-scrollable-anchor';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { modalDefaultDonation } from '../../../../config/donation-settings';
|
||||
import Cup from '../../assets/icons/cup';
|
||||
@ -25,13 +24,13 @@ import './Donation.css';
|
||||
const mapStateToProps = createSelector(
|
||||
isDonationModalOpenSelector,
|
||||
recentlyClaimedBlockSelector,
|
||||
(show, recentlyClaimedBlock) => ({
|
||||
(show: boolean, recentlyClaimedBlock: string) => ({
|
||||
show,
|
||||
recentlyClaimedBlock
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
closeDonationModal,
|
||||
@ -40,16 +39,13 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
const propTypes = {
|
||||
activeDonors: PropTypes.number,
|
||||
closeDonationModal: PropTypes.func.isRequired,
|
||||
executeGA: PropTypes.func,
|
||||
location: PropTypes.shape({
|
||||
hash: PropTypes.string,
|
||||
pathname: PropTypes.string
|
||||
}),
|
||||
recentlyClaimedBlock: PropTypes.string,
|
||||
show: PropTypes.bool
|
||||
type DonateModalProps = {
|
||||
activeDonors: number;
|
||||
closeDonationModal: typeof closeDonationModal;
|
||||
executeGA: typeof executeGA;
|
||||
location: WindowLocation | undefined;
|
||||
recentlyClaimedBlock: string;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
function DonateModal({
|
||||
@ -58,10 +54,14 @@ function DonateModal({
|
||||
executeGA,
|
||||
location,
|
||||
recentlyClaimedBlock
|
||||
}) {
|
||||
}: DonateModalProps): JSX.Element {
|
||||
const [closeLabel, setCloseLabel] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
const handleProcessing = (duration, amount, action) => {
|
||||
const handleProcessing = (
|
||||
duration: string,
|
||||
amount: number,
|
||||
action: string
|
||||
) => {
|
||||
executeGA({
|
||||
type: 'event',
|
||||
data: {
|
||||
@ -107,6 +107,7 @@ function DonateModal({
|
||||
const handleModalHide = () => {
|
||||
// If modal is open on a SuperBlock page
|
||||
if (isLocationSuperBlock(location)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
goToAnchor('claim-cert-block');
|
||||
}
|
||||
};
|
||||
@ -114,6 +115,8 @@ function DonateModal({
|
||||
const blockDonationText = (
|
||||
<div className=' text-center block-modal-text'>
|
||||
<div className='donation-icon-container'>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<Cup className='donation-icon' />
|
||||
</div>
|
||||
<Row>
|
||||
@ -131,6 +134,8 @@ function DonateModal({
|
||||
const progressDonationText = (
|
||||
<div className='text-center progress-modal-text'>
|
||||
<div className='donation-icon-container'>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore */}
|
||||
<Heart className='donation-icon' />
|
||||
</div>
|
||||
<Row>
|
||||
@ -175,6 +180,5 @@ function DonateModal({
|
||||
}
|
||||
|
||||
DonateModal.displayName = 'DonateModal';
|
||||
DonateModal.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DonateModal);
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
export const DonationSupportText = () => {
|
||||
export const DonationSupportText = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
@ -13,7 +13,7 @@ export const DonationSupportText = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DonationText = () => {
|
||||
export const DonationText = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
@ -24,7 +24,7 @@ export const DonationText = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DonationOptionsText = () => {
|
||||
export const DonationOptionsText = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
@ -42,7 +42,7 @@ export const DonationOptionsText = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const DonationOptionsAlertText = () => {
|
||||
export const DonationOptionsAlertText = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<p>
|
@ -1,112 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Loader } from '../../components/helpers';
|
||||
import { scriptLoader, scriptRemover } from '../../utils/script-loaders';
|
||||
|
||||
export class PayPalButtonScriptLoader extends Component {
|
||||
state = { isSdkLoaded: window.paypal && true, isSubscription: true };
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { isSubscription } = props;
|
||||
if (isSubscription !== state.isSubscription) {
|
||||
return { isSubscription: isSubscription };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!window.paypal) {
|
||||
this.loadScript(this.props.isSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.isSubscription !== this.state.isSubscription ||
|
||||
prevProps.style !== this.props.style
|
||||
) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ isSdkLoaded: false });
|
||||
this.loadScript(this.state.isSubscription, true);
|
||||
}
|
||||
}
|
||||
|
||||
loadScript(subscription, deleteScript) {
|
||||
if (deleteScript) scriptRemover('paypal-sdk');
|
||||
let queries = `?client-id=${this.props.clientId}&disable-funding=credit,bancontact,blik,eps,giropay,ideal,mybank,p24,sepa,sofort,venmo`;
|
||||
if (subscription) queries += '&vault=true&intent=subscription';
|
||||
|
||||
scriptLoader(
|
||||
'paypal-sdk',
|
||||
true,
|
||||
`https://www.paypal.com/sdk/js${queries}`,
|
||||
this.onScriptLoad
|
||||
);
|
||||
}
|
||||
|
||||
onScriptLoad = () => {
|
||||
this.setState({ isSdkLoaded: true });
|
||||
};
|
||||
|
||||
captureOneTimePayment(data, actions) {
|
||||
return actions.order.capture().then(details => {
|
||||
return this.props.onApprove(details, data);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSdkLoaded, isSubscription } = this.state;
|
||||
const {
|
||||
onApprove,
|
||||
onError,
|
||||
onCancel,
|
||||
createSubscription,
|
||||
createOrder,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
if (!isSdkLoaded) return <Loader />;
|
||||
|
||||
const Button = window.paypal.Buttons.driver('react', {
|
||||
React,
|
||||
ReactDOM
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
createOrder={isSubscription ? null : createOrder}
|
||||
createSubscription={isSubscription ? createSubscription : null}
|
||||
onApprove={
|
||||
isSubscription
|
||||
? (data, actions) => onApprove(data, actions)
|
||||
: (data, actions) => this.captureOneTimePayment(data, actions)
|
||||
}
|
||||
onCancel={onCancel}
|
||||
onError={onError}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const propTypes = {
|
||||
clientId: PropTypes.string,
|
||||
createOrder: PropTypes.func,
|
||||
createSubscription: PropTypes.func,
|
||||
donationAmount: PropTypes.number,
|
||||
donationDuration: PropTypes.string,
|
||||
isSubscription: PropTypes.bool,
|
||||
onApprove: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onError: PropTypes.func,
|
||||
style: PropTypes.object
|
||||
};
|
||||
|
||||
PayPalButtonScriptLoader.displayName = 'PayPalButtonScriptLoader';
|
||||
PayPalButtonScriptLoader.propTypes = propTypes;
|
||||
|
||||
export default PayPalButtonScriptLoader;
|
188
client/src/components/Donation/PayPalButtonScriptLoader.tsx
Normal file
188
client/src/components/Donation/PayPalButtonScriptLoader.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
/* eslint-disable camelcase */
|
||||
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';
|
||||
|
||||
type PayPalButtonScriptLoaderProps = {
|
||||
clientId: string;
|
||||
createOrder: (
|
||||
data: unknown,
|
||||
actions: {
|
||||
order: {
|
||||
create: (arg0: {
|
||||
purchase_units: {
|
||||
amount: { currency_code: string; value: string };
|
||||
}[];
|
||||
}) => unknown;
|
||||
};
|
||||
}
|
||||
) => unknown;
|
||||
createSubscription: (
|
||||
data: unknown,
|
||||
actions: {
|
||||
subscription: { create: (arg0: { plan_id: string | null }) => unknown };
|
||||
}
|
||||
) => unknown;
|
||||
isSubscription: boolean;
|
||||
onApprove: (
|
||||
data: AddDonationData,
|
||||
actions?: { order: { capture: () => Promise<unknown> } }
|
||||
) => unknown;
|
||||
onCancel: () => unknown;
|
||||
onError: () => unknown;
|
||||
style: unknown;
|
||||
planId: string | null;
|
||||
};
|
||||
|
||||
type PayPalButtonScriptLoaderState = {
|
||||
isSdkLoaded: boolean;
|
||||
isSubscription: boolean;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
paypal: {
|
||||
Buttons: {
|
||||
driver: (
|
||||
react: string,
|
||||
{ React, ReactDOM }: { React: unknown; ReactDOM: unknown }
|
||||
) => unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class PayPalButtonScriptLoader extends Component<
|
||||
PayPalButtonScriptLoaderProps,
|
||||
PayPalButtonScriptLoaderState
|
||||
> {
|
||||
// Lint says that paypal does not exist on window
|
||||
state = { isSdkLoaded: window.paypal ? true : false, isSubscription: true };
|
||||
|
||||
static displayName = 'PayPalButtonScriptLoader';
|
||||
|
||||
static getDerivedStateFromProps(
|
||||
props: PayPalButtonScriptLoaderProps,
|
||||
state: PayPalButtonScriptLoaderState
|
||||
): { isSubscription: boolean } | null {
|
||||
const { isSubscription } = props;
|
||||
if (isSubscription !== state.isSubscription) {
|
||||
return { isSubscription: isSubscription };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (!window.paypal) {
|
||||
this.loadScript(this.props.isSubscription, false);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: {
|
||||
isSubscription: boolean;
|
||||
style: unknown;
|
||||
}): void {
|
||||
if (
|
||||
prevProps.isSubscription !== this.state.isSubscription ||
|
||||
prevProps.style !== this.props.style
|
||||
) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({ isSdkLoaded: false });
|
||||
this.loadScript(this.state.isSubscription, true);
|
||||
}
|
||||
}
|
||||
|
||||
loadScript(subscription: boolean, deleteScript: boolean | undefined): void {
|
||||
if (deleteScript) scriptRemover('paypal-sdk');
|
||||
let queries = `?client-id=${this.props.clientId}&disable-funding=credit,bancontact,blik,eps,giropay,ideal,mybank,p24,sepa,sofort,venmo`;
|
||||
if (subscription) queries += '&vault=true&intent=subscription';
|
||||
|
||||
scriptLoader(
|
||||
'paypal-sdk',
|
||||
true,
|
||||
`https://www.paypal.com/sdk/js${queries}`,
|
||||
this.onScriptLoad,
|
||||
'paypal'
|
||||
);
|
||||
}
|
||||
|
||||
onScriptLoad = (): void => {
|
||||
this.setState({ isSdkLoaded: true });
|
||||
};
|
||||
|
||||
captureOneTimePayment(
|
||||
data: unknown,
|
||||
actions: { order: { capture: () => Promise<unknown> } }
|
||||
): unknown {
|
||||
return actions.order.capture().then((details: unknown) => {
|
||||
// TODO: this looks like a bug (it probably should not be passing details)
|
||||
// but the api does not care what data it gets (yet). If we start to use
|
||||
// that, this will need to be changed.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return this.props.onApprove(details, data);
|
||||
});
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const {
|
||||
isSdkLoaded,
|
||||
isSubscription
|
||||
}: { isSdkLoaded: boolean; isSubscription: boolean } = this.state;
|
||||
const {
|
||||
onApprove,
|
||||
onError,
|
||||
onCancel,
|
||||
createSubscription,
|
||||
createOrder,
|
||||
style
|
||||
} = this.props;
|
||||
|
||||
if (!isSdkLoaded) return <Loader />;
|
||||
|
||||
// TODO: fill in the full list of props instead of any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Button: React.ComponentType<any> = window.paypal.Buttons.driver(
|
||||
'react',
|
||||
{
|
||||
React,
|
||||
ReactDOM
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as React.ComponentType<any>;
|
||||
|
||||
return (
|
||||
<Button
|
||||
createOrder={isSubscription ? null : createOrder}
|
||||
createSubscription={isSubscription ? createSubscription : null}
|
||||
onApprove={
|
||||
isSubscription
|
||||
? (
|
||||
data: AddDonationData,
|
||||
actions: { order: { capture: () => Promise<unknown> } }
|
||||
) => onApprove(data, actions)
|
||||
: (
|
||||
data: {
|
||||
[key: string]: unknown;
|
||||
error: string | null;
|
||||
},
|
||||
actions: { order: { capture: () => Promise<unknown> } }
|
||||
) => this.captureOneTimePayment(data, actions)
|
||||
}
|
||||
onCancel={onCancel}
|
||||
onError={onError}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PayPalButtonScriptLoader.displayName = 'PayPalButtonScriptLoader';
|
||||
|
||||
export default PayPalButtonScriptLoader;
|
@ -1,42 +1,104 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
paypalConfigurator,
|
||||
paypalConfigTypes
|
||||
paypalConfigTypes,
|
||||
defaultDonation
|
||||
} from '../../../../config/donation-settings';
|
||||
import envData from '../../../../config/env.json';
|
||||
import { signInLoadingSelector, userSelector } from '../../redux';
|
||||
import PayPalButtonScriptLoader from './PayPalButtonScriptLoader';
|
||||
|
||||
const { paypalClientId, deploymentEnv } = envData;
|
||||
export class PaypalButton extends Component {
|
||||
constructor(props) {
|
||||
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;
|
||||
skipAddDonation?: boolean;
|
||||
t: (label: string) => string;
|
||||
theme: string;
|
||||
isSubscription?: boolean;
|
||||
};
|
||||
|
||||
type PaypalButtonState = {
|
||||
amount: number;
|
||||
duration: string;
|
||||
planId: string | null;
|
||||
};
|
||||
|
||||
export interface AddDonationData {
|
||||
redirecting: boolean;
|
||||
processing: boolean;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
state = {};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
static getDerivedStateFromProps(props: PaypalButtonProps): PaypalButtonState {
|
||||
const { donationAmount, donationDuration } = props;
|
||||
|
||||
const configurationObj = paypalConfigurator(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const configurationObj: {
|
||||
amount: number;
|
||||
duration: string;
|
||||
planId: string | null;
|
||||
} = paypalConfigurator(
|
||||
donationAmount,
|
||||
donationDuration,
|
||||
paypalConfigTypes[deploymentEnv || 'staging']
|
||||
);
|
||||
if (state === configurationObj) {
|
||||
return null;
|
||||
}
|
||||
// re-implement it as a deep comparison.
|
||||
// if (state === configurationObj) {
|
||||
// return null;
|
||||
// }
|
||||
return { ...configurationObj };
|
||||
}
|
||||
|
||||
handleApproval = (data, isSubscription) => {
|
||||
handleApproval = (data: AddDonationData, isSubscription: boolean): void => {
|
||||
const { amount, duration } = this.state;
|
||||
const { skipAddDonation = false } = this.props;
|
||||
|
||||
@ -49,13 +111,14 @@ export class PaypalButton extends Component {
|
||||
|
||||
// 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() {
|
||||
render(): JSX.Element | null {
|
||||
const { duration, planId, amount } = this.state;
|
||||
const { t, theme } = this.props;
|
||||
const isSubscription = duration !== 'onetime';
|
||||
@ -66,10 +129,21 @@ export class PaypalButton extends Component {
|
||||
|
||||
return (
|
||||
<div className={'paypal-buttons-container'}>
|
||||
{/* help needed */}
|
||||
<PayPalButtonScriptLoader
|
||||
amount={amount}
|
||||
clientId={paypalClientId}
|
||||
createOrder={(data, actions) => {
|
||||
createOrder={(
|
||||
data: unknown,
|
||||
actions: {
|
||||
order: {
|
||||
create: (arg0: {
|
||||
purchase_units: {
|
||||
amount: { currency_code: string; value: string };
|
||||
}[];
|
||||
}) => unknown;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return actions.order.create({
|
||||
purchase_units: [
|
||||
{
|
||||
@ -81,17 +155,25 @@ export class PaypalButton extends Component {
|
||||
]
|
||||
});
|
||||
}}
|
||||
createSubscription={(data, actions) => {
|
||||
createSubscription={(
|
||||
data: unknown,
|
||||
actions: {
|
||||
subscription: {
|
||||
create: (arg0: { plan_id: string | null }) => unknown;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
return actions.subscription.create({
|
||||
plan_id: planId
|
||||
});
|
||||
}}
|
||||
isSubscription={isSubscription}
|
||||
onApprove={data => {
|
||||
onApprove={(data: AddDonationData) => {
|
||||
this.handleApproval(data, isSubscription);
|
||||
}}
|
||||
onCancel={() => {
|
||||
this.props.onDonationStateChange({
|
||||
redirecting: false,
|
||||
processing: false,
|
||||
success: false,
|
||||
error: t('donate.failed-pay')
|
||||
@ -99,12 +181,13 @@ export class PaypalButton extends Component {
|
||||
}}
|
||||
onError={() =>
|
||||
this.props.onDonationStateChange({
|
||||
redirecting: false,
|
||||
processing: false,
|
||||
success: false,
|
||||
error: t('donate.try-again')
|
||||
})
|
||||
}
|
||||
plantId={planId}
|
||||
planId={planId}
|
||||
style={{
|
||||
tagline: false,
|
||||
height: 43,
|
||||
@ -116,28 +199,15 @@ export class PaypalButton extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const propTypes = {
|
||||
addDonation: PropTypes.func,
|
||||
donationAmount: PropTypes.number,
|
||||
donationDuration: PropTypes.string,
|
||||
handleProcessing: PropTypes.func,
|
||||
isDonating: PropTypes.bool,
|
||||
onDonationStateChange: PropTypes.func,
|
||||
skipAddDonation: PropTypes.bool,
|
||||
t: PropTypes.func.isRequired,
|
||||
theme: PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
signInLoadingSelector,
|
||||
({ isDonating }, showLoading) => ({
|
||||
({ isDonating }: { isDonating: boolean }, showLoading: boolean) => ({
|
||||
isDonating,
|
||||
showLoading
|
||||
})
|
||||
);
|
||||
|
||||
PaypalButton.displayName = 'PaypalButton';
|
||||
PaypalButton.propTypes = propTypes;
|
||||
|
||||
export default connect(mapStateToProps)(withTranslation()(PaypalButton));
|
@ -1,7 +1,8 @@
|
||||
import { Grid, Row, Col, Alert } from '@freecodecamp/react-bootstrap';
|
||||
import type { TFunction } from 'i18next';
|
||||
import React, { useEffect } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
@ -98,6 +98,8 @@ interface Donation {
|
||||
customerId: string;
|
||||
startDate: Date;
|
||||
}
|
||||
// TODO: Verify if the body has and needs this Donation type. The api seems to
|
||||
// just need the body to exist, but doesn't seem to use the properties.
|
||||
export function addDonation(body: Donation): Promise<void> {
|
||||
return post('/donate/add-donation', body);
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ const amountsConfig = {
|
||||
onetime: [2500, 5000, 7500, 10000, 15000]
|
||||
};
|
||||
const defaultAmount = {
|
||||
month: 500
|
||||
month: 500,
|
||||
onetime: 7500
|
||||
};
|
||||
const defaultDonation = {
|
||||
donationAmount: defaultAmount['month'],
|
||||
@ -75,7 +76,7 @@ const paypalConfigTypes = {
|
||||
|
||||
const paypalConfigurator = (donationAmount, donationDuration, paypalConfig) => {
|
||||
if (donationDuration === 'onetime') {
|
||||
return { amount: donationAmount, duration: donationDuration };
|
||||
return { amount: donationAmount, duration: donationDuration, planId: null };
|
||||
}
|
||||
return {
|
||||
amount: donationAmount,
|
||||
|
Reference in New Issue
Block a user