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:
Nicolás Restrepo
2021-08-04 06:21:11 -04:00
committed by GitHub
parent 8886a9396c
commit e34ec814ef
12 changed files with 467 additions and 239 deletions

View File

@ -4375,6 +4375,14 @@
"@types/react": "*" "@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": { "@types/react-spinkit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/react-spinkit/-/react-spinkit-3.0.7.tgz", "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", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" "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": { "bl": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -9624,6 +9641,12 @@
"token-types": "^3.0.0" "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": { "filesize": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz",
"integrity": "sha1-Cr+2rYNXGLn7Te8GdOBmV6lUN1w=" "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": { "nanoid": {
"version": "3.1.23", "version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
@ -21183,7 +21212,11 @@
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"optional": true "optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
}, },
"glob-parent": { "glob-parent": {
"version": "3.1.0", "version": "3.1.0",

View File

@ -53,6 +53,7 @@
"@freecodecamp/strip-comments": "3.0.1", "@freecodecamp/strip-comments": "3.0.1",
"@loadable/component": "5.15.0", "@loadable/component": "5.15.0",
"@reach/router": "1.3.4", "@reach/router": "1.3.4",
"@types/react-scrollable-anchor": "^0.6.1",
"algoliasearch": "4.10.3", "algoliasearch": "4.10.3",
"assert": "2.0.0", "assert": "2.0.0",
"babel-plugin-preval": "5.0.0", "babel-plugin-preval": "5.0.0",

View File

@ -1,17 +1,16 @@
import { Alert, Button } from '@freecodecamp/react-bootstrap'; import { Alert, Button } from '@freecodecamp/react-bootstrap';
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Spinner from 'react-spinkit'; import Spinner from 'react-spinkit';
import './Donation.css'; import './Donation.css';
const propTypes = { type DonateCompletionProps = {
error: PropTypes.string, error: string | null;
processing: PropTypes.bool, processing: boolean;
redirecting: PropTypes.bool, redirecting: boolean;
reset: PropTypes.func.isRequired, reset: () => unknown;
success: PropTypes.bool success: boolean;
}; };
function DonateCompletion({ function DonateCompletion({
@ -20,7 +19,7 @@ function DonateCompletion({
success, success,
redirecting, redirecting,
error = null error = null
}) { }: DonateCompletionProps): JSX.Element {
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
const { t } = useTranslation(); const { t } = useTranslation();
const style = const style =
@ -68,6 +67,5 @@ function DonateCompletion({
} }
DonateCompletion.displayName = 'DonateCompletion'; DonateCompletion.displayName = 'DonateCompletion';
DonateCompletion.propTypes = propTypes;
export default DonateCompletion; export default DonateCompletion;

View File

@ -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 */ /* eslint-disable no-nested-ternary */
import { import {
Col, Col,
@ -7,7 +11,6 @@ import {
ToggleButton, ToggleButton,
ToggleButtonGroup ToggleButtonGroup
} from '@freecodecamp/react-bootstrap'; } from '@freecodecamp/react-bootstrap';
import PropTypes from 'prop-types';
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';
@ -30,27 +33,41 @@ import {
userSelector userSelector
} from '../../redux'; } from '../../redux';
import Spacer from '../helpers/spacer'; import Spacer from '../helpers/spacer';
import DonateCompletion from './DonateCompletion'; import DonateCompletion from './DonateCompletion';
import type { AddDonationData } from './PaypalButton';
import PaypalButton from './PaypalButton'; import PaypalButton from './PaypalButton';
import './Donation.css'; import './Donation.css';
const numToCommas = num => const numToCommas = (num: number): string =>
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
const propTypes = { type DonateFormState = {
addDonation: PropTypes.func, donationAmount: number;
defaultTheme: PropTypes.string, donationDuration: string;
donationFormState: PropTypes.object, processing: boolean;
email: PropTypes.string, redirecting: boolean;
handleProcessing: PropTypes.func, success: boolean;
isDonating: PropTypes.bool, error: string;
isMinimalForm: PropTypes.bool, };
isSignedIn: PropTypes.bool,
showLoading: PropTypes.bool.isRequired, type DonateFormProps = {
t: PropTypes.func.isRequired, addDonation: (data: unknown) => unknown;
theme: PropTypes.string, defaultTheme?: string;
updateDonationFormState: PropTypes.func 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( const mapStateToProps = createSelector(
@ -58,7 +75,12 @@ const mapStateToProps = createSelector(
isSignedInSelector, isSignedInSelector,
donationFormStateSelector, donationFormStateSelector,
userSelector, userSelector,
(showLoading, isSignedIn, donationFormState, { email, theme }) => ({ (
showLoading: DonateFormProps['showLoading'],
isSignedIn: DonateFormProps['isSignedIn'],
donationFormState: DonateFormState,
{ email, theme }: { email: string; theme: string }
) => ({
isSignedIn, isSignedIn,
showLoading, showLoading,
donationFormState, donationFormState,
@ -72,11 +94,17 @@ const mapDispatchToProps = {
updateDonationFormState updateDonationFormState
}; };
class DonateForm extends Component { class DonateForm extends Component<DonateFormProps, DonateFormState> {
constructor(...args) { static displayName = 'DonateForm';
super(...args); 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; this.amounts = amountsConfig;
const initialAmountAndDuration = this.props.isMinimalForm const initialAmountAndDuration = this.props.isMinimalForm
@ -85,7 +113,10 @@ class DonateForm extends Component {
this.state = { this.state = {
...initialAmountAndDuration, ...initialAmountAndDuration,
processing: false processing: false,
redirecting: false,
success: false,
error: ''
}; };
this.onDonationStateChange = this.onDonationStateChange.bind(this); this.onDonationStateChange = this.onDonationStateChange.bind(this);
@ -101,23 +132,26 @@ class DonateForm extends Component {
this.resetDonation(); this.resetDonation();
} }
onDonationStateChange(donationState) { onDonationStateChange(donationState: AddDonationData) {
// scroll to top // scroll to top
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.props.updateDonationFormState(donationState); this.props.updateDonationFormState(donationState);
} }
getActiveDonationAmount(durationSelected, amountSelected) { getActiveDonationAmount(
durationSelected: 'month' | 'onetime',
amountSelected: number
): number {
return this.amounts[durationSelected].includes(amountSelected) return this.amounts[durationSelected].includes(amountSelected)
? amountSelected ? amountSelected
: defaultAmount[durationSelected] || this.amounts[durationSelected][0]; : defaultAmount[durationSelected] || this.amounts[durationSelected][0];
} }
convertToTimeContributed(amount) { convertToTimeContributed(amount: number) {
return numToCommas((amount / 100) * 50); return numToCommas((amount / 100) * 50);
} }
getFormattedAmountLabel(amount) { getFormattedAmountLabel(amount: number): string {
return `${numToCommas(amount / 100)}`; return `${numToCommas(amount / 100)}`;
} }
@ -141,17 +175,17 @@ class DonateForm extends Component {
return donationBtnLabel; return donationBtnLabel;
} }
handleSelectDuration(donationDuration) { handleSelectDuration(donationDuration: 'month' | 'onetime') {
const donationAmount = this.getActiveDonationAmount(donationDuration, 0); const donationAmount = this.getActiveDonationAmount(donationDuration, 0);
this.setState({ donationDuration, donationAmount }); this.setState({ donationDuration, donationAmount });
} }
handleSelectAmount(donationAmount) { handleSelectAmount(donationAmount: number) {
this.setState({ donationAmount }); this.setState({ donationAmount });
} }
renderAmountButtons(duration) { renderAmountButtons(duration: 'month' | 'onetime') {
return this.amounts[duration].map(amount => ( return this.amounts[duration].map((amount: number) => (
<ToggleButton <ToggleButton
className='amount-value' className='amount-value'
id={`${this.durations[duration]}-donation-${amount}`} id={`${this.durations[duration]}-donation-${amount}`}
@ -195,36 +229,41 @@ class DonateForm extends Component {
id='Duration' id='Duration'
onSelect={this.handleSelectDuration} onSelect={this.handleSelectDuration}
> >
{Object.keys(this.durations).map(duration => ( {(Object.keys(this.durations) as ['month' | 'onetime']).map(
<Tab duration => (
eventKey={duration} <Tab
key={duration} eventKey={duration}
title={this.durations[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 /> <Spacer />
{this.renderDonationDescription()} <h3>{t('donate.gift-amount')}</h3>
</div> <div>
</Tab> <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> </Tabs>
</div> </div>
) : null; ) : null;
} }
hideAmountOptionsCB(hide) { hideAmountOptionsCB(hide: boolean) {
this.setState({ processing: hide }); this.setState({ processing: hide });
} }
@ -271,7 +310,13 @@ class DonateForm extends Component {
return this.props.updateDonationFormState({ ...defaultDonationFormState }); return this.props.updateDonationFormState({ ...defaultDonationFormState });
} }
renderCompletion(props) { renderCompletion(props: {
processing: boolean;
redirecting: boolean;
success: boolean;
error: string | null;
reset: () => unknown;
}) {
return <DonateCompletion {...props} />; return <DonateCompletion {...props} />;
} }
@ -333,9 +378,7 @@ class DonateForm extends Component {
reset: this.resetDonation reset: this.resetDonation
})} })}
<div className={processing || redirecting ? 'hide' : ''}> <div className={processing || redirecting ? 'hide' : ''}>
{isMinimalForm {isMinimalForm ? this.renderModalForm() : this.renderPageForm()}
? this.renderModalForm(processing)
: this.renderPageForm(processing)}
</div> </div>
</> </>
); );
@ -343,7 +386,6 @@ class DonateForm extends Component {
} }
DonateForm.displayName = 'DonateForm'; DonateForm.displayName = 'DonateForm';
DonateForm.propTypes = propTypes;
export default connect( export default connect(
mapStateToProps, mapStateToProps,

View File

@ -1,11 +1,10 @@
/* eslint-disable max-len */
import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap'; 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 React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { goToAnchor } from 'react-scrollable-anchor'; import { goToAnchor } from 'react-scrollable-anchor';
import { bindActionCreators } from 'redux'; import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { modalDefaultDonation } from '../../../../config/donation-settings'; import { modalDefaultDonation } from '../../../../config/donation-settings';
import Cup from '../../assets/icons/cup'; import Cup from '../../assets/icons/cup';
@ -25,13 +24,13 @@ import './Donation.css';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
isDonationModalOpenSelector, isDonationModalOpenSelector,
recentlyClaimedBlockSelector, recentlyClaimedBlockSelector,
(show, recentlyClaimedBlock) => ({ (show: boolean, recentlyClaimedBlock: string) => ({
show, show,
recentlyClaimedBlock recentlyClaimedBlock
}) })
); );
const mapDispatchToProps = dispatch => const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
bindActionCreators( bindActionCreators(
{ {
closeDonationModal, closeDonationModal,
@ -40,16 +39,13 @@ const mapDispatchToProps = dispatch =>
dispatch dispatch
); );
const propTypes = { type DonateModalProps = {
activeDonors: PropTypes.number, activeDonors: number;
closeDonationModal: PropTypes.func.isRequired, closeDonationModal: typeof closeDonationModal;
executeGA: PropTypes.func, executeGA: typeof executeGA;
location: PropTypes.shape({ location: WindowLocation | undefined;
hash: PropTypes.string, recentlyClaimedBlock: string;
pathname: PropTypes.string show: boolean;
}),
recentlyClaimedBlock: PropTypes.string,
show: PropTypes.bool
}; };
function DonateModal({ function DonateModal({
@ -58,10 +54,14 @@ function DonateModal({
executeGA, executeGA,
location, location,
recentlyClaimedBlock recentlyClaimedBlock
}) { }: DonateModalProps): JSX.Element {
const [closeLabel, setCloseLabel] = React.useState(false); const [closeLabel, setCloseLabel] = React.useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const handleProcessing = (duration, amount, action) => { const handleProcessing = (
duration: string,
amount: number,
action: string
) => {
executeGA({ executeGA({
type: 'event', type: 'event',
data: { data: {
@ -107,6 +107,7 @@ function DonateModal({
const handleModalHide = () => { const handleModalHide = () => {
// If modal is open on a SuperBlock page // If modal is open on a SuperBlock page
if (isLocationSuperBlock(location)) { if (isLocationSuperBlock(location)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
goToAnchor('claim-cert-block'); goToAnchor('claim-cert-block');
} }
}; };
@ -114,6 +115,8 @@ function DonateModal({
const blockDonationText = ( const blockDonationText = (
<div className=' text-center block-modal-text'> <div className=' text-center block-modal-text'>
<div className='donation-icon-container'> <div className='donation-icon-container'>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Cup className='donation-icon' /> <Cup className='donation-icon' />
</div> </div>
<Row> <Row>
@ -131,6 +134,8 @@ function DonateModal({
const progressDonationText = ( const progressDonationText = (
<div className='text-center progress-modal-text'> <div className='text-center progress-modal-text'>
<div className='donation-icon-container'> <div className='donation-icon-container'>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Heart className='donation-icon' /> <Heart className='donation-icon' />
</div> </div>
<Row> <Row>
@ -175,6 +180,5 @@ function DonateModal({
} }
DonateModal.displayName = 'DonateModal'; DonateModal.displayName = 'DonateModal';
DonateModal.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(DonateModal); export default connect(mapStateToProps, mapDispatchToProps)(DonateModal);

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
export const DonationSupportText = () => { export const DonationSupportText = (): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
@ -13,7 +13,7 @@ export const DonationSupportText = () => {
); );
}; };
export const DonationText = () => { export const DonationText = (): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
@ -24,7 +24,7 @@ export const DonationText = () => {
); );
}; };
export const DonationOptionsText = () => { export const DonationOptionsText = (): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
@ -42,7 +42,7 @@ export const DonationOptionsText = () => {
); );
}; };
export const DonationOptionsAlertText = () => { export const DonationOptionsAlertText = (): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<p> <p>

View File

@ -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;

View 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;

View File

@ -1,42 +1,104 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import PropTypes from 'prop-types';
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 { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { import {
paypalConfigurator, paypalConfigurator,
paypalConfigTypes paypalConfigTypes,
defaultDonation
} from '../../../../config/donation-settings'; } from '../../../../config/donation-settings';
import envData from '../../../../config/env.json'; import envData from '../../../../config/env.json';
import { signInLoadingSelector, userSelector } from '../../redux'; import { signInLoadingSelector, userSelector } from '../../redux';
import PayPalButtonScriptLoader from './PayPalButtonScriptLoader'; import PayPalButtonScriptLoader from './PayPalButtonScriptLoader';
const { paypalClientId, deploymentEnv } = envData; type PaypalButtonProps = {
export class PaypalButton extends Component { addDonation: (data: AddDonationData) => void;
constructor(props) { 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); super(props);
this.handleApproval = this.handleApproval.bind(this); this.handleApproval = this.handleApproval.bind(this);
} }
state = {}; static getDerivedStateFromProps(props: PaypalButtonProps): PaypalButtonState {
static getDerivedStateFromProps(props, state) {
const { donationAmount, donationDuration } = props; const { donationAmount, donationDuration } = props;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const configurationObj = paypalConfigurator( const configurationObj: {
amount: number;
duration: string;
planId: string | null;
} = paypalConfigurator(
donationAmount, donationAmount,
donationDuration, donationDuration,
paypalConfigTypes[deploymentEnv || 'staging'] paypalConfigTypes[deploymentEnv || 'staging']
); );
if (state === configurationObj) { // re-implement it as a deep comparison.
return null; // if (state === configurationObj) {
} // return null;
// }
return { ...configurationObj }; return { ...configurationObj };
} }
handleApproval = (data, isSubscription) => { handleApproval = (data: AddDonationData, isSubscription: boolean): void => {
const { amount, duration } = this.state; const { amount, duration } = this.state;
const { skipAddDonation = false } = this.props; const { skipAddDonation = false } = this.props;
@ -49,13 +111,14 @@ export class PaypalButton extends Component {
// Show success anytime because the payment has gone through paypal // Show success anytime because the payment has gone through paypal
this.props.onDonationStateChange({ this.props.onDonationStateChange({
redirecting: false,
processing: false, processing: false,
success: true, success: true,
error: data.error ? data.error : null error: data.error ? data.error : null
}); });
}; };
render() { render(): JSX.Element | null {
const { duration, planId, amount } = this.state; const { duration, planId, amount } = this.state;
const { t, theme } = this.props; const { t, theme } = this.props;
const isSubscription = duration !== 'onetime'; const isSubscription = duration !== 'onetime';
@ -66,10 +129,21 @@ export class PaypalButton extends Component {
return ( return (
<div className={'paypal-buttons-container'}> <div className={'paypal-buttons-container'}>
{/* help needed */}
<PayPalButtonScriptLoader <PayPalButtonScriptLoader
amount={amount}
clientId={paypalClientId} 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({ return actions.order.create({
purchase_units: [ 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({ return actions.subscription.create({
plan_id: planId plan_id: planId
}); });
}} }}
isSubscription={isSubscription} isSubscription={isSubscription}
onApprove={data => { onApprove={(data: AddDonationData) => {
this.handleApproval(data, isSubscription); this.handleApproval(data, isSubscription);
}} }}
onCancel={() => { onCancel={() => {
this.props.onDonationStateChange({ this.props.onDonationStateChange({
redirecting: false,
processing: false, processing: false,
success: false, success: false,
error: t('donate.failed-pay') error: t('donate.failed-pay')
@ -99,12 +181,13 @@ export class PaypalButton extends Component {
}} }}
onError={() => onError={() =>
this.props.onDonationStateChange({ this.props.onDonationStateChange({
redirecting: false,
processing: false, processing: false,
success: false, success: false,
error: t('donate.try-again') error: t('donate.try-again')
}) })
} }
plantId={planId} planId={planId}
style={{ style={{
tagline: false, tagline: false,
height: 43, 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( const mapStateToProps = createSelector(
userSelector, userSelector,
signInLoadingSelector, signInLoadingSelector,
({ isDonating }, showLoading) => ({ ({ isDonating }: { isDonating: boolean }, showLoading: boolean) => ({
isDonating, isDonating,
showLoading showLoading
}) })
); );
PaypalButton.displayName = 'PaypalButton'; PaypalButton.displayName = 'PaypalButton';
PaypalButton.propTypes = propTypes;
export default connect(mapStateToProps)(withTranslation()(PaypalButton)); export default connect(mapStateToProps)(withTranslation()(PaypalButton));

View File

@ -1,7 +1,8 @@
import { Grid, Row, Col, Alert } from '@freecodecamp/react-bootstrap'; import { Grid, Row, Col, Alert } from '@freecodecamp/react-bootstrap';
import type { TFunction } from 'i18next';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { TFunction, withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';

View File

@ -98,6 +98,8 @@ interface Donation {
customerId: string; customerId: string;
startDate: Date; 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> { export function addDonation(body: Donation): Promise<void> {
return post('/donate/add-donation', body); return post('/donate/add-donation', body);
} }

View File

@ -8,7 +8,8 @@ const amountsConfig = {
onetime: [2500, 5000, 7500, 10000, 15000] onetime: [2500, 5000, 7500, 10000, 15000]
}; };
const defaultAmount = { const defaultAmount = {
month: 500 month: 500,
onetime: 7500
}; };
const defaultDonation = { const defaultDonation = {
donationAmount: defaultAmount['month'], donationAmount: defaultAmount['month'],
@ -75,7 +76,7 @@ const paypalConfigTypes = {
const paypalConfigurator = (donationAmount, donationDuration, paypalConfig) => { const paypalConfigurator = (donationAmount, donationDuration, paypalConfig) => {
if (donationDuration === 'onetime') { if (donationDuration === 'onetime') {
return { amount: donationAmount, duration: donationDuration }; return { amount: donationAmount, duration: donationDuration, planId: null };
} }
return { return {
amount: donationAmount, amount: donationAmount,