mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): AB test adding mutitier donation modal (#51539)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -474,6 +474,8 @@
|
|||||||
"confirm-one-time": "Confirm your one-time donation of ${{usd}}:",
|
"confirm-one-time": "Confirm your one-time donation of ${{usd}}:",
|
||||||
"confirm-monthly": "Confirm your donation of ${{usd}} / month:",
|
"confirm-monthly": "Confirm your donation of ${{usd}} / month:",
|
||||||
"confirm-yearly": "Confirm your donation of ${{usd}} / year:",
|
"confirm-yearly": "Confirm your donation of ${{usd}} / year:",
|
||||||
|
"confirm-multitier": "Donating ${{usd}} / month:",
|
||||||
|
"edit-amount": "edit amount",
|
||||||
"wallet-label": "${{usd}} donation to freeCodeCamp",
|
"wallet-label": "${{usd}} donation to freeCodeCamp",
|
||||||
"wallet-label-1": "${{usd}} / month donation to freeCodeCamp",
|
"wallet-label-1": "${{usd}} / month donation to freeCodeCamp",
|
||||||
"your-donation": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world.",
|
"your-donation": "Your ${{usd}} donation will provide {{hours}} hours of learning to people around the world.",
|
||||||
@@ -490,6 +492,7 @@
|
|||||||
"progress-modal-cta-8": "Donate now to help us develop new courses on emerging tools and programming concepts.",
|
"progress-modal-cta-8": "Donate now to help us develop new courses on emerging tools and programming concepts.",
|
||||||
"progress-modal-cta-9": "Donate now to support our math for developers curriculum.",
|
"progress-modal-cta-9": "Donate now to support our math for developers curriculum.",
|
||||||
"progress-modal-cta-10": "Donate now to help us develop free professional programming certifications for all.",
|
"progress-modal-cta-10": "Donate now to help us develop free professional programming certifications for all.",
|
||||||
|
"help-us-develop": "Help us develop free professional programming certifications for all.",
|
||||||
"nicely-done": "Nicely done. You just completed {{block}}.",
|
"nicely-done": "Nicely done. You just completed {{block}}.",
|
||||||
"credit-card": "Credit Card",
|
"credit-card": "Credit Card",
|
||||||
"credit-card-2": "Or donate with a credit card:",
|
"credit-card-2": "Or donate with a credit card:",
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Spacer size='medium' />
|
||||||
<Row>
|
<Row>
|
||||||
<Col sm={4} smOffset={4} xs={6} xsOffset={3}>
|
<Col sm={4} smOffset={4} xs={6} xsOffset={3}>
|
||||||
{isDonationSubmitted && donationCloseBtn}
|
{isDonationSubmitted && donationCloseBtn}
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import { connect } from 'react-redux';
|
|||||||
import Spinner from 'react-spinkit';
|
import Spinner from 'react-spinkit';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
|
import { Button } from '@freecodecamp/react-bootstrap';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
amountsConfig,
|
|
||||||
durationsConfig,
|
|
||||||
defaultDonation,
|
defaultDonation,
|
||||||
modalDefaultDonation,
|
DonationAmount,
|
||||||
type DonationConfig
|
type DonationConfig
|
||||||
} from '../../../../shared/config/donation-settings';
|
} from '../../../../shared/config/donation-settings';
|
||||||
import { defaultDonationFormState } from '../../redux';
|
import { defaultDonationFormState } from '../../redux';
|
||||||
@@ -25,6 +24,12 @@ import {
|
|||||||
} from '../../redux/selectors';
|
} from '../../redux/selectors';
|
||||||
import Spacer from '../helpers/spacer';
|
import Spacer from '../helpers/spacer';
|
||||||
import { Themes } from '../settings/theme';
|
import { Themes } from '../settings/theme';
|
||||||
|
import { DonateFormState } from '../../redux/types';
|
||||||
|
import {
|
||||||
|
CENTS_IN_DOLLAR,
|
||||||
|
convertToTimeContributed,
|
||||||
|
formattedAmountLabel
|
||||||
|
} from './utils';
|
||||||
import DonateCompletion from './donate-completion';
|
import DonateCompletion from './donate-completion';
|
||||||
import PatreonButton from './patreon-button';
|
import PatreonButton from './patreon-button';
|
||||||
import PaypalButton from './paypal-button';
|
import PaypalButton from './paypal-button';
|
||||||
@@ -41,28 +46,6 @@ import {
|
|||||||
|
|
||||||
import './donation.css';
|
import './donation.css';
|
||||||
|
|
||||||
const numToCommas = (num: number) =>
|
|
||||||
num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
|
||||||
|
|
||||||
// the number is used to indicate to the doner about how much hours of free education their dontation will provide.
|
|
||||||
const contributedHoursOfFreeEduction = 50;
|
|
||||||
const convertAmountToUSD = 100;
|
|
||||||
const convertToTimeContributed = (amount: number) =>
|
|
||||||
numToCommas((amount / convertAmountToUSD) * contributedHoursOfFreeEduction);
|
|
||||||
const formattedAmountLabel = (amount: number) =>
|
|
||||||
numToCommas(amount / convertAmountToUSD);
|
|
||||||
|
|
||||||
type DonateFormState = {
|
|
||||||
processing: boolean;
|
|
||||||
redirecting: boolean;
|
|
||||||
success: boolean;
|
|
||||||
error: string;
|
|
||||||
loading: {
|
|
||||||
stripe: boolean;
|
|
||||||
paypal: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type DonateFormComponentState = DonationConfig;
|
type DonateFormComponentState = DonationConfig;
|
||||||
|
|
||||||
type PostCharge = (data: {
|
type PostCharge = (data: {
|
||||||
@@ -83,6 +66,8 @@ type DonateFormProps = {
|
|||||||
defaultTheme?: Themes;
|
defaultTheme?: Themes;
|
||||||
email: string;
|
email: string;
|
||||||
handleProcessing?: () => void;
|
handleProcessing?: () => void;
|
||||||
|
editAmount?: () => void;
|
||||||
|
selectedDonationAmount?: DonationAmount;
|
||||||
donationFormState: DonateFormState;
|
donationFormState: DonateFormState;
|
||||||
isMinimalForm?: boolean;
|
isMinimalForm?: boolean;
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
@@ -135,17 +120,10 @@ const PaymentButtonsLoader = () => {
|
|||||||
|
|
||||||
class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
||||||
static displayName = 'DonateForm';
|
static displayName = 'DonateForm';
|
||||||
durations: { month: 'monthly'; onetime: 'one-time' };
|
|
||||||
amounts: { month: number[]; onetime: number[] };
|
|
||||||
constructor(props: DonateFormProps) {
|
constructor(props: DonateFormProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.durations = durationsConfig;
|
const initialAmountAndDuration: DonationConfig = defaultDonation;
|
||||||
this.amounts = amountsConfig;
|
|
||||||
|
|
||||||
const initialAmountAndDuration: DonationConfig = this.props.isMinimalForm
|
|
||||||
? modalDefaultDonation
|
|
||||||
: defaultDonation;
|
|
||||||
|
|
||||||
this.state = { ...initialAmountAndDuration };
|
this.state = { ...initialAmountAndDuration };
|
||||||
|
|
||||||
@@ -187,8 +165,9 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
paymentMethodId,
|
paymentMethodId,
|
||||||
handleAuthentication
|
handleAuthentication
|
||||||
}: PostPayment): void => {
|
}: PostPayment): void => {
|
||||||
const { donationAmount: amount, donationDuration: duration } = this.state;
|
const { donationAmount, donationDuration: duration } = this.state;
|
||||||
const { paymentContext, email } = this.props;
|
const { paymentContext, email, selectedDonationAmount } = this.props;
|
||||||
|
const amount = selectedDonationAmount || donationAmount;
|
||||||
|
|
||||||
this.props.postCharge({
|
this.props.postCharge({
|
||||||
paymentProvider,
|
paymentProvider,
|
||||||
@@ -210,7 +189,7 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderButtonGroup() {
|
renderButtonGroup() {
|
||||||
const { donationAmount, donationDuration } = this.state;
|
const { donationAmount: defaultAmount, donationDuration } = this.state;
|
||||||
const {
|
const {
|
||||||
donationFormState: { loading, processing },
|
donationFormState: { loading, processing },
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
@@ -218,24 +197,44 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
t,
|
t,
|
||||||
isMinimalForm,
|
isMinimalForm,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
isDonating
|
isDonating,
|
||||||
|
editAmount,
|
||||||
|
selectedDonationAmount
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const donationAmount: DonationAmount =
|
||||||
|
selectedDonationAmount || defaultAmount;
|
||||||
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
const priorityTheme = defaultTheme ? defaultTheme : theme;
|
||||||
const isOneTime = donationDuration === 'one-time';
|
const walletlabel = `${t('donate.wallet-label-1', {
|
||||||
const walletlabel = `${t(
|
usd: donationAmount / CENTS_IN_DOLLAR
|
||||||
isOneTime ? 'donate.wallet-label' : 'donate.wallet-label-1',
|
})}:`;
|
||||||
{ usd: donationAmount / convertAmountToUSD }
|
console.log(formattedAmountLabel(donationAmount));
|
||||||
)}:`;
|
|
||||||
const showMinimalPayments = isSignedIn && (isMinimalForm || !isDonating);
|
const showMinimalPayments = isSignedIn && (isMinimalForm || !isDonating);
|
||||||
|
const confirmationMessage = t('donate.confirm-monthly', {
|
||||||
|
usd: formattedAmountLabel(donationAmount)
|
||||||
|
});
|
||||||
|
const confirmationWithEditAmount = (
|
||||||
|
<>
|
||||||
|
{t('donate.confirm-multitier', {
|
||||||
|
usd: formattedAmountLabel(donationAmount)
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Button bsStyle='primary' className='btn-link' onClick={editAmount}>
|
||||||
|
{t('donate.edit-amount')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmationClass = () => {
|
||||||
|
if (editAmount) return 'edit-amount-confirmation';
|
||||||
|
if (isMinimalForm) return 'donation-label-modal';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<b className={isMinimalForm ? 'donation-label-modal' : ''}>
|
<b className={confirmationClass()}>
|
||||||
{t('donate.confirm-monthly', {
|
{editAmount ? confirmationWithEditAmount : confirmationMessage}
|
||||||
usd: formattedAmountLabel(donationAmount)
|
|
||||||
})}
|
|
||||||
</b>
|
</b>
|
||||||
<Spacer size='medium' />
|
<Spacer size={editAmount ? 'small' : 'medium'} />
|
||||||
<fieldset className={'donate-btn-group security-legend'}>
|
<fieldset className={'donate-btn-group security-legend'}>
|
||||||
<legend>
|
<legend>
|
||||||
<SecurityLockIcon />
|
<SecurityLockIcon />
|
||||||
@@ -263,7 +262,10 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
theme={priorityTheme}
|
theme={priorityTheme}
|
||||||
/>
|
/>
|
||||||
{(!loading.stripe || !loading.paypal) && (
|
{(!loading.stripe || !loading.paypal) && (
|
||||||
<PatreonButton postPayment={this.postPayment} />
|
<PatreonButton
|
||||||
|
postPayment={this.postPayment}
|
||||||
|
donationAmount={donationAmount}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{showMinimalPayments && (
|
{showMinimalPayments && (
|
||||||
<>
|
<>
|
||||||
@@ -283,18 +285,12 @@ class DonateForm extends Component<DonateFormProps, DonateFormComponentState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderPageForm() {
|
renderPageForm() {
|
||||||
const { donationAmount, donationDuration } = this.state;
|
const { donationAmount } = this.state;
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const usd = formattedAmountLabel(donationAmount);
|
const usd = formattedAmountLabel(donationAmount);
|
||||||
const hours = convertToTimeContributed(donationAmount);
|
const hours = convertToTimeContributed(donationAmount);
|
||||||
|
const donationDescription = t('donate.your-donation-2', { usd, hours });
|
||||||
|
|
||||||
let donationDescription = t('donate.your-donation-3', { usd, hours });
|
|
||||||
|
|
||||||
if (donationDuration === 'one-time') {
|
|
||||||
donationDescription = t('donate.your-donation', { usd, hours });
|
|
||||||
} else if (donationDuration === 'month') {
|
|
||||||
donationDescription = t('donate.your-donation-2', { usd, hours });
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className='donation-description'>{donationDescription}</p>
|
<p className='donation-description'>{donationDescription}</p>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap';
|
import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap';
|
||||||
|
import { Tabs, TabsContent, TabsTrigger, TabsList } from '@freecodecamp/ui';
|
||||||
import { WindowLocation } from '@reach/router';
|
import { WindowLocation } from '@reach/router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -7,10 +8,14 @@ import { useFeature } from '@growthbook/growthbook-react';
|
|||||||
import { goToAnchor } from 'react-scrollable-anchor';
|
import { goToAnchor } from 'react-scrollable-anchor';
|
||||||
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { PaymentContext } from '../../../../shared/config/donation-settings';
|
import {
|
||||||
|
PaymentContext,
|
||||||
|
subscriptionAmounts,
|
||||||
|
defaultDonation,
|
||||||
|
defaultTierAmount
|
||||||
|
} from '../../../../shared/config/donation-settings';
|
||||||
import BearProgressModal from '../../assets/images/components/bear-progress-modal';
|
import BearProgressModal from '../../assets/images/components/bear-progress-modal';
|
||||||
import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal';
|
import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal';
|
||||||
|
|
||||||
import { closeDonationModal, executeGA } from '../../redux/actions';
|
import { closeDonationModal, executeGA } from '../../redux/actions';
|
||||||
import {
|
import {
|
||||||
isDonationModalOpenSelector,
|
isDonationModalOpenSelector,
|
||||||
@@ -20,6 +25,7 @@ import { isLocationSuperBlock } from '../../utils/path-parsers';
|
|||||||
import { playTone } from '../../utils/tone';
|
import { playTone } from '../../utils/tone';
|
||||||
import { Spacer } from '../helpers';
|
import { Spacer } from '../helpers';
|
||||||
import DonateForm from './donate-form';
|
import DonateForm from './donate-form';
|
||||||
|
import { formattedAmountLabel, convertToTimeContributed } from './utils';
|
||||||
|
|
||||||
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
|
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
|
||||||
|
|
||||||
@@ -79,7 +85,12 @@ function DonateModal({
|
|||||||
const [ctaNumber, setCtaNumber] = useState(0);
|
const [ctaNumber, setCtaNumber] = useState(0);
|
||||||
const [isDisabled, setIsDisabled] = useState(true);
|
const [isDisabled, setIsDisabled] = useState(true);
|
||||||
const [showSkipButton, setShowSkipButton] = useState(false);
|
const [showSkipButton, setShowSkipButton] = useState(false);
|
||||||
|
const [showDonateForm, setShowDonateForm] = useState(true);
|
||||||
|
const [donationAmount, setDonationAmount] = useState(
|
||||||
|
defaultDonation.donationAmount
|
||||||
|
);
|
||||||
const loadElementsIndividually = useFeature('load_elements_individually').on;
|
const loadElementsIndividually = useFeature('load_elements_individually').on;
|
||||||
|
const showMultiTier = useFeature('multi-tier').on;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// test wheather the conversions are being distributed properly
|
// test wheather the conversions are being distributed properly
|
||||||
@@ -119,6 +130,13 @@ function DonateModal({
|
|||||||
if (show) setCtaNumber(getctaNumberBetween1To10());
|
if (show) setCtaNumber(getctaNumberBetween1To10());
|
||||||
}, [show]);
|
}, [show]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showMultiTier) {
|
||||||
|
setShowDonateForm(false);
|
||||||
|
setDonationAmount(defaultTierAmount);
|
||||||
|
}
|
||||||
|
}, [showMultiTier]);
|
||||||
|
|
||||||
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)) {
|
||||||
@@ -126,11 +144,8 @@ function DonateModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const donationText = (
|
const modalHeader = (
|
||||||
<div className=' text-center block-modal-text'>
|
<div className=' text-center block-modal-text'>
|
||||||
<div className='donation-icon-container'>
|
|
||||||
<RenderIlustration recentlyClaimedBlock={recentlyClaimedBlock} />
|
|
||||||
</div>
|
|
||||||
<Row>
|
<Row>
|
||||||
{!closeLabel && (
|
{!closeLabel && (
|
||||||
<Col sm={10} smOffset={1} xs={12}>
|
<Col sm={10} smOffset={1} xs={12}>
|
||||||
@@ -143,13 +158,146 @@ function DonateModal({
|
|||||||
})}
|
})}
|
||||||
</b>
|
</b>
|
||||||
)}
|
)}
|
||||||
<b>{t(`donate.progress-modal-cta-${ctaNumber}`)}</b>
|
{showMultiTier ? (
|
||||||
|
<h1>{t('donate.help-us-develop')}</h1>
|
||||||
|
) : (
|
||||||
|
<b>{t(`donate.progress-modal-cta-${ctaNumber}`)}</b>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
<Spacer size='small' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const closeButtonRow = (
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
sm={4}
|
||||||
|
smOffset={4}
|
||||||
|
xs={8}
|
||||||
|
xsOffset={2}
|
||||||
|
className={showSkipButton ? 'no-delay-fade-in' : 'no-opacity'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
bsSize='sm'
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-link close-button'
|
||||||
|
onClick={closeDonationModal}
|
||||||
|
tabIndex='0'
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{closeLabel ? t('buttons.close') : t('buttons.ask-later')}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectionTabs = (
|
||||||
|
<Row className={'donate-btn-group'}>
|
||||||
|
<Col
|
||||||
|
xs={12}
|
||||||
|
className={loadElementsIndividually && 'two-seconds-delay-fade-in'}
|
||||||
|
>
|
||||||
|
<b>
|
||||||
|
{t('donate.confirm-monthly', {
|
||||||
|
usd: formattedAmountLabel(donationAmount)
|
||||||
|
})}
|
||||||
|
</b>
|
||||||
|
<Spacer size='small' />
|
||||||
|
<Tabs
|
||||||
|
className={'donate-btn-group'}
|
||||||
|
defaultValue={donationAmount.toString()}
|
||||||
|
>
|
||||||
|
<TabsList className='nav-lists'>
|
||||||
|
{subscriptionAmounts.map(value => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={value}
|
||||||
|
value={value.toString()}
|
||||||
|
onClick={() => setDonationAmount(value)}
|
||||||
|
>
|
||||||
|
${formattedAmountLabel(value)}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
<Spacer size='small' />
|
||||||
|
{subscriptionAmounts.map(value => {
|
||||||
|
const usd = formattedAmountLabel(donationAmount);
|
||||||
|
const hours = convertToTimeContributed(donationAmount);
|
||||||
|
const donationDescription = t('donate.your-donation-2', {
|
||||||
|
usd,
|
||||||
|
hours
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContent
|
||||||
|
key={value}
|
||||||
|
className='tab-content'
|
||||||
|
value={value.toString()}
|
||||||
|
>
|
||||||
|
<p>{donationDescription}</p>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsStyle='primary'
|
||||||
|
className='text-center confirm-donation-btn donate-btn-group'
|
||||||
|
type='submit'
|
||||||
|
onClick={() => setShowDonateForm(true)}
|
||||||
|
>
|
||||||
|
{t('buttons.donate')}
|
||||||
|
</Button>
|
||||||
|
<Spacer size='medium' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
|
||||||
|
const donationFormRow = (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
xs={12}
|
||||||
|
className={loadElementsIndividually && 'two-seconds-delay-fade-in'}
|
||||||
|
>
|
||||||
|
<DonateForm
|
||||||
|
handleProcessing={handleProcessing}
|
||||||
|
isMinimalForm={true}
|
||||||
|
paymentContext={PaymentContext.Modal}
|
||||||
|
editAmount={
|
||||||
|
showMultiTier ? () => setShowDonateForm(false) : undefined
|
||||||
|
}
|
||||||
|
selectedDonationAmount={donationAmount}
|
||||||
|
/>
|
||||||
|
<Spacer size='medium' />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
|
||||||
|
const multiTierModalBody = (
|
||||||
|
<>
|
||||||
|
<div className={showDonateForm ? 'hide' : ''}>
|
||||||
|
{modalHeader}
|
||||||
|
{selectionTabs}
|
||||||
|
{closeButtonRow}
|
||||||
|
</div>
|
||||||
|
<div className={!showDonateForm ? 'hide' : ''}>
|
||||||
|
{donationFormRow}
|
||||||
|
{closeLabel && closeButtonRow}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultModalBody = (
|
||||||
|
<>
|
||||||
|
{modalHeader}
|
||||||
|
{donationFormRow}
|
||||||
|
{closeButtonRow}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
bsSize='lg'
|
bsSize='lg'
|
||||||
@@ -158,42 +306,10 @@ function DonateModal({
|
|||||||
show={show}
|
show={show}
|
||||||
>
|
>
|
||||||
<Modal.Body className={'no-delay-fade-in'}>
|
<Modal.Body className={'no-delay-fade-in'}>
|
||||||
{donationText}
|
<div className='donation-icon-container'>
|
||||||
<Spacer size='medium' />
|
<RenderIlustration recentlyClaimedBlock={recentlyClaimedBlock} />
|
||||||
<Row>
|
</div>
|
||||||
<Col
|
{showMultiTier ? multiTierModalBody : defaultModalBody}
|
||||||
xs={12}
|
|
||||||
className={loadElementsIndividually && 'two-seconds-delay-fade-in'}
|
|
||||||
>
|
|
||||||
<DonateForm
|
|
||||||
handleProcessing={handleProcessing}
|
|
||||||
isMinimalForm={true}
|
|
||||||
paymentContext={PaymentContext.Modal}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Spacer size='medium' />
|
|
||||||
<Row>
|
|
||||||
<Col
|
|
||||||
sm={4}
|
|
||||||
smOffset={4}
|
|
||||||
xs={8}
|
|
||||||
xsOffset={2}
|
|
||||||
className={showSkipButton ? 'no-delay-fade-in' : 'no-opacity'}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
block={true}
|
|
||||||
bsSize='sm'
|
|
||||||
bsStyle='primary'
|
|
||||||
className='btn-link'
|
|
||||||
onClick={closeDonationModal}
|
|
||||||
tabIndex='0'
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
{closeLabel ? t('buttons.close') : t('buttons.ask-later')}
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -286,15 +286,48 @@ li.disabled > a {
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-modal p,
|
|
||||||
.donation-modal b {
|
.donation-modal b {
|
||||||
text-align: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.donation-modal p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
.donation-label-modal {
|
.donation-label-modal {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-amount-confirmation {
|
||||||
|
width: 350px !important;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-modal h1 {
|
||||||
|
font-family: var(--font-family-sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-modal [role='tablist'] button {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.donation-modal [role='tablist'] button:hover:not([data-state='active']) {
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
color: var(--quaternary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.donation-modal [role='tablist'] button[data-state='active'] {
|
||||||
|
background-color: var(--quaternary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-icon-container {
|
.donation-icon-container {
|
||||||
@@ -350,10 +383,6 @@ li.disabled > a {
|
|||||||
margin: 40px;
|
margin: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.donation-modal p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.donation-modal .modal-title {
|
.donation-modal .modal-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
|
DonationAmount,
|
||||||
donationUrls,
|
donationUrls,
|
||||||
patreonDefaultPledgeAmount,
|
|
||||||
PaymentProvider
|
PaymentProvider
|
||||||
} from '../../../../shared/config/donation-settings';
|
} from '../../../../shared/config/donation-settings';
|
||||||
import envData from '../../../config/env.json';
|
import envData from '../../../config/env.json';
|
||||||
@@ -14,21 +14,21 @@ const { patreonClientId }: { patreonClientId: string | null } = envData as {
|
|||||||
|
|
||||||
interface PatreonButtonProps {
|
interface PatreonButtonProps {
|
||||||
postPayment: (arg0: PostPayment) => void;
|
postPayment: (arg0: PostPayment) => void;
|
||||||
|
donationAmount: DonationAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PatreonButton = ({
|
const PatreonButton = ({
|
||||||
postPayment
|
postPayment,
|
||||||
|
donationAmount
|
||||||
}: PatreonButtonProps): JSX.Element | null => {
|
}: PatreonButtonProps): JSX.Element | null => {
|
||||||
if (
|
if (!patreonClientId || !donationAmount || !donationUrls.successUrl) {
|
||||||
!patreonClientId ||
|
|
||||||
!patreonDefaultPledgeAmount ||
|
|
||||||
!donationUrls.successUrl
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientId = `&client_id=${patreonClientId}`;
|
const clientId = `&client_id=${patreonClientId}`;
|
||||||
const pledgeLevel = `$&min_cents=${patreonDefaultPledgeAmount}`;
|
|
||||||
|
// current Patreon pledge flow does not support custom amounts, it must be a tier
|
||||||
|
const pledgeLevel = `$&min_cents=${donationAmount}`;
|
||||||
const v2Params = '&scope=identity%20identity[email]';
|
const v2Params = '&scope=identity%20identity[email]';
|
||||||
const redirectUri = `&redirect_uri=${donationUrls.successUrl}`;
|
const redirectUri = `&redirect_uri=${donationUrls.successUrl}`;
|
||||||
const href = `https://www.patreon.com/oauth2/become-patron?response_type=code${pledgeLevel}${clientId}${redirectUri}${v2Params}`;
|
const href = `https://www.patreon.com/oauth2/become-patron?response_type=code${pledgeLevel}${clientId}${redirectUri}${v2Params}`;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const numToCommas = (num: number) => Intl.NumberFormat('en-US').format(num);
|
||||||
|
const EDUCATION_HOURS_PER_DOLLAR = 50;
|
||||||
|
export const CENTS_IN_DOLLAR = 100;
|
||||||
|
export const convertToTimeContributed = (amount: number) =>
|
||||||
|
numToCommas((amount / CENTS_IN_DOLLAR) * EDUCATION_HOURS_PER_DOLLAR);
|
||||||
|
export const formattedAmountLabel = (amount: number) =>
|
||||||
|
numToCommas(amount / CENTS_IN_DOLLAR);
|
||||||
@@ -47,3 +47,14 @@ interface DefaultDonationFormState {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error: null | string;
|
error: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DonateFormState {
|
||||||
|
processing: boolean;
|
||||||
|
redirecting: boolean;
|
||||||
|
success: boolean;
|
||||||
|
error: string;
|
||||||
|
loading: {
|
||||||
|
stripe: boolean;
|
||||||
|
paypal: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,21 @@
|
|||||||
// Configuration for client side
|
// Configuration for client side
|
||||||
|
|
||||||
export type DonationAmount = 500 | 1000 | 2000 | 3000 | 4000 | 5000;
|
export type DonationAmount = 500 | 1000 | 2000 | 4000;
|
||||||
export type DonationDuration = 'one-time' | 'month';
|
export type DonationDuration = 'one-time' | 'month';
|
||||||
export interface DonationConfig {
|
export interface DonationConfig {
|
||||||
donationAmount: DonationAmount;
|
donationAmount: DonationAmount;
|
||||||
donationDuration: DonationDuration;
|
donationDuration: DonationDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const durationsConfig: {
|
export const subscriptionAmounts: DonationAmount[] = [500, 1000, 2000, 4000];
|
||||||
month: 'monthly';
|
|
||||||
onetime: 'one-time';
|
|
||||||
} = {
|
|
||||||
month: 'monthly',
|
|
||||||
onetime: 'one-time'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const amountsConfig = {
|
|
||||||
month: [1000, 2000, 3000, 4000, 5000],
|
|
||||||
onetime: [2500, 5000, 7500, 10000, 15000]
|
|
||||||
};
|
|
||||||
export const defaultAmount: { month: 500; onetime: 7500 } = {
|
|
||||||
month: 500,
|
|
||||||
onetime: 7500
|
|
||||||
};
|
|
||||||
export const defaultDonation: DonationConfig = {
|
export const defaultDonation: DonationConfig = {
|
||||||
donationAmount: defaultAmount.month,
|
|
||||||
donationDuration: 'month'
|
|
||||||
};
|
|
||||||
export const modalDefaultDonation: DonationConfig = {
|
|
||||||
donationAmount: 500,
|
donationAmount: 500,
|
||||||
donationDuration: 'month'
|
donationDuration: 'month'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultTierAmount = 2000;
|
||||||
|
|
||||||
export const onetimeSKUConfig = {
|
export const onetimeSKUConfig = {
|
||||||
live: [
|
live: [
|
||||||
{ amount: '15000', id: 'sku_IElisJHup0nojP' },
|
{ amount: '15000', id: 'sku_IElisJHup0nojP' },
|
||||||
@@ -127,13 +111,6 @@ export const donationUrls = {
|
|||||||
cancelUrl: 'https://freecodecamp.org/donate'
|
cancelUrl: 'https://freecodecamp.org/donate'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const patreonDefaultPledgeAmount = 500;
|
|
||||||
|
|
||||||
export const aBTestConfig = {
|
|
||||||
isTesting: true,
|
|
||||||
type: 'secureIconButtonOnly'
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum PaymentContext {
|
export enum PaymentContext {
|
||||||
Modal = 'modal',
|
Modal = 'modal',
|
||||||
DonatePage = 'donate page',
|
DonatePage = 'donate page',
|
||||||
|
|||||||
Reference in New Issue
Block a user