From e34ec814ef54988de3c61046a1d252625440911a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Restrepo?= Date: Wed, 4 Aug 2021 06:21:11 -0400 Subject: [PATCH] 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 * Update client/src/components/Donation/DonationModal.tsx Co-authored-by: Shaun Hamilton * Update client/src/components/Donation/DonationModal.tsx Co-authored-by: Shaun Hamilton * Update client/src/components/Donation/DonateForm.tsx Co-authored-by: Oliver Eyton-Williams * Update client/src/components/Donation/DonateForm.tsx Co-authored-by: Oliver Eyton-Williams * Delete console.log client/src/components/Donation/DonationModal.tsx Co-authored-by: Shaun Hamilton * applied changes requested * fix: readjust default one time amount * fix types Co-authored-by: Oliver Eyton-Williams * chore: restore comments.json * fix: type assertion * fix: specific DonateForm props * Apply suggestions from code review Co-authored-by: Oliver Eyton-Williams * Update client/src/components/Donation/PaypalButton.tsx Co-authored-by: Oliver Eyton-Williams * fix:set default stat for paypalbutton Co-authored-by: Oliver Eyton-Williams Co-authored-by: Shaun Hamilton Co-authored-by: Ahmad Abdolsaheb --- client/package-lock.json | 35 +++- client/package.json | 1 + ...nateCompletion.js => DonateCompletion.tsx} | 16 +- .../{DonateForm.js => DonateForm.tsx} | 158 +++++++++------ .../{DonationModal.js => DonationModal.tsx} | 40 ++-- ...mponents.js => DonationTextComponents.tsx} | 8 +- .../Donation/PayPalButtonScriptLoader.js | 112 ----------- .../Donation/PayPalButtonScriptLoader.tsx | 188 ++++++++++++++++++ .../{PaypalButton.js => PaypalButton.tsx} | 138 +++++++++---- client/src/pages/donate.tsx | 3 +- client/src/utils/ajax.ts | 2 + config/donation-settings.js | 5 +- 12 files changed, 467 insertions(+), 239 deletions(-) rename client/src/components/Donation/{DonateCompletion.js => DonateCompletion.tsx} (86%) rename client/src/components/Donation/{DonateForm.js => DonateForm.tsx} (69%) rename client/src/components/Donation/{DonationModal.js => DonationModal.tsx} (82%) rename client/src/components/Donation/{DonationTextComponents.js => DonationTextComponents.tsx} (81%) delete mode 100644 client/src/components/Donation/PayPalButtonScriptLoader.js create mode 100644 client/src/components/Donation/PayPalButtonScriptLoader.tsx rename client/src/components/Donation/{PaypalButton.js => PaypalButton.tsx} (51%) diff --git a/client/package-lock.json b/client/package-lock.json index db1f80d995..9c2a9647cf 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index 634c48b57f..7c2466af4f 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/components/Donation/DonateCompletion.js b/client/src/components/Donation/DonateCompletion.tsx similarity index 86% rename from client/src/components/Donation/DonateCompletion.js rename to client/src/components/Donation/DonateCompletion.tsx index 892b47c82c..3a6c3a32d9 100644 --- a/client/src/components/Donation/DonateCompletion.js +++ b/client/src/components/Donation/DonateCompletion.tsx @@ -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; diff --git a/client/src/components/Donation/DonateForm.js b/client/src/components/Donation/DonateForm.tsx similarity index 69% rename from client/src/components/Donation/DonateForm.js rename to client/src/components/Donation/DonateForm.tsx index 61ab4cf107..0aa879dbef 100644 --- a/client/src/components/Donation/DonateForm.js +++ b/client/src/components/Donation/DonateForm.tsx @@ -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 { + 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) => ( - {Object.keys(this.durations).map(duration => ( - - -

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

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

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

+
+ + {this.renderAmountButtons(duration)} + + + {this.renderDonationDescription()} +
+ + ) + )} ) : 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 ; } @@ -333,9 +378,7 @@ class DonateForm extends Component { reset: this.resetDonation })}
- {isMinimalForm - ? this.renderModalForm(processing) - : this.renderPageForm(processing)} + {isMinimalForm ? this.renderModalForm() : this.renderPageForm()}
); @@ -343,7 +386,6 @@ class DonateForm extends Component { } DonateForm.displayName = 'DonateForm'; -DonateForm.propTypes = propTypes; export default connect( mapStateToProps, diff --git a/client/src/components/Donation/DonationModal.js b/client/src/components/Donation/DonationModal.tsx similarity index 82% rename from client/src/components/Donation/DonationModal.js rename to client/src/components/Donation/DonationModal.tsx index 558a84e1cf..bca1161559 100644 --- a/client/src/components/Donation/DonationModal.js +++ b/client/src/components/Donation/DonationModal.tsx @@ -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) => 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 = (
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */}
@@ -131,6 +134,8 @@ function DonateModal({ const progressDonationText = (
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */}
@@ -175,6 +180,5 @@ function DonateModal({ } DonateModal.displayName = 'DonateModal'; -DonateModal.propTypes = propTypes; export default connect(mapStateToProps, mapDispatchToProps)(DonateModal); diff --git a/client/src/components/Donation/DonationTextComponents.js b/client/src/components/Donation/DonationTextComponents.tsx similarity index 81% rename from client/src/components/Donation/DonationTextComponents.js rename to client/src/components/Donation/DonationTextComponents.tsx index 8951177f4b..077be916dd 100644 --- a/client/src/components/Donation/DonationTextComponents.js +++ b/client/src/components/Donation/DonationTextComponents.tsx @@ -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 (

diff --git a/client/src/components/Donation/PayPalButtonScriptLoader.js b/client/src/components/Donation/PayPalButtonScriptLoader.js deleted file mode 100644 index ac73a54a68..0000000000 --- a/client/src/components/Donation/PayPalButtonScriptLoader.js +++ /dev/null @@ -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 ; - - const Button = window.paypal.Buttons.driver('react', { - React, - ReactDOM - }); - - return ( -