{
}
renderPageForm() {
- const { donationAmount, donationDuration } = this.state;
+ const { donationAmount } = this.state;
const { t } = this.props;
const usd = formattedAmountLabel(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 (
<>
{donationDescription}
diff --git a/client/src/components/Donation/donation-modal.tsx b/client/src/components/Donation/donation-modal.tsx
index 6702437dad8..db509e61833 100644
--- a/client/src/components/Donation/donation-modal.tsx
+++ b/client/src/components/Donation/donation-modal.tsx
@@ -1,4 +1,5 @@
import { Modal, Button, Col, Row } from '@freecodecamp/react-bootstrap';
+import { Tabs, TabsContent, TabsTrigger, TabsList } from '@freecodecamp/ui';
import { WindowLocation } from '@reach/router';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -7,10 +8,14 @@ import { useFeature } from '@growthbook/growthbook-react';
import { goToAnchor } from 'react-scrollable-anchor';
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
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 BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal';
-
import { closeDonationModal, executeGA } from '../../redux/actions';
import {
isDonationModalOpenSelector,
@@ -20,6 +25,7 @@ import { isLocationSuperBlock } from '../../utils/path-parsers';
import { playTone } from '../../utils/tone';
import { Spacer } from '../helpers';
import DonateForm from './donate-form';
+import { formattedAmountLabel, convertToTimeContributed } from './utils';
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
@@ -79,7 +85,12 @@ function DonateModal({
const [ctaNumber, setCtaNumber] = useState(0);
const [isDisabled, setIsDisabled] = useState(true);
const [showSkipButton, setShowSkipButton] = useState(false);
+ const [showDonateForm, setShowDonateForm] = useState(true);
+ const [donationAmount, setDonationAmount] = useState(
+ defaultDonation.donationAmount
+ );
const loadElementsIndividually = useFeature('load_elements_individually').on;
+ const showMultiTier = useFeature('multi-tier').on;
const { t } = useTranslation();
// test wheather the conversions are being distributed properly
@@ -119,6 +130,13 @@ function DonateModal({
if (show) setCtaNumber(getctaNumberBetween1To10());
}, [show]);
+ useEffect(() => {
+ if (showMultiTier) {
+ setShowDonateForm(false);
+ setDonationAmount(defaultTierAmount);
+ }
+ }, [showMultiTier]);
+
const handleModalHide = () => {
// If modal is open on a SuperBlock page
if (isLocationSuperBlock(location)) {
@@ -126,11 +144,8 @@ function DonateModal({
}
};
- const donationText = (
+ const modalHeader = (
-
-
-
{!closeLabel && (
@@ -143,13 +158,146 @@ function DonateModal({
})}
)}
- {t(`donate.progress-modal-cta-${ctaNumber}`)}
+ {showMultiTier ? (
+ {t('donate.help-us-develop')}
+ ) : (
+ {t(`donate.progress-modal-cta-${ctaNumber}`)}
+ )}
)}
+
);
+ const closeButtonRow = (
+ <>
+
+
+
+ {closeLabel ? t('buttons.close') : t('buttons.ask-later')}
+
+
+
+ >
+ );
+
+ const selectionTabs = (
+
+
+
+ {t('donate.confirm-monthly', {
+ usd: formattedAmountLabel(donationAmount)
+ })}
+
+
+
+
+ {subscriptionAmounts.map(value => (
+ setDonationAmount(value)}
+ >
+ ${formattedAmountLabel(value)}
+
+ ))}
+
+
+ {subscriptionAmounts.map(value => {
+ const usd = formattedAmountLabel(donationAmount);
+ const hours = convertToTimeContributed(donationAmount);
+ const donationDescription = t('donate.your-donation-2', {
+ usd,
+ hours
+ });
+
+ return (
+
+ {donationDescription}
+
+ );
+ })}
+
+ setShowDonateForm(true)}
+ >
+ {t('buttons.donate')}
+
+
+
+
+ );
+
+ const donationFormRow = (
+
+
+ setShowDonateForm(false) : undefined
+ }
+ selectedDonationAmount={donationAmount}
+ />
+
+
+
+ );
+
+ const multiTierModalBody = (
+ <>
+
+ {modalHeader}
+ {selectionTabs}
+ {closeButtonRow}
+
+
+ {donationFormRow}
+ {closeLabel && closeButtonRow}
+
+ >
+ );
+
+ const defaultModalBody = (
+ <>
+ {modalHeader}
+ {donationFormRow}
+ {closeButtonRow}
+ >
+ );
+
return (
- {donationText}
-
-
-
-
-
-
-
-
-
-
- {closeLabel ? t('buttons.close') : t('buttons.ask-later')}
-
-
-
+
+
+
+ {showMultiTier ? multiTierModalBody : defaultModalBody}
);
diff --git a/client/src/components/Donation/donation.css b/client/src/components/Donation/donation.css
index 59d88904e9b..ffb31b9baf1 100644
--- a/client/src/components/Donation/donation.css
+++ b/client/src/components/Donation/donation.css
@@ -286,15 +286,48 @@ li.disabled > a {
font-size: 1.2rem;
}
-.donation-modal p,
.donation-modal b {
- text-align: center;
- font-size: 1rem;
width: 100%;
display: inline-block;
}
+
+.donation-modal p {
+ font-size: 0.9rem;
+}
.donation-label-modal {
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 {
@@ -350,10 +383,6 @@ li.disabled > a {
margin: 40px;
}
- .donation-modal p {
- font-size: 1.1rem;
- }
-
.donation-modal .modal-title {
font-weight: 700;
font-size: 1.5rem;
diff --git a/client/src/components/Donation/patreon-button.tsx b/client/src/components/Donation/patreon-button.tsx
index 8507cd777c3..ac39b456af8 100644
--- a/client/src/components/Donation/patreon-button.tsx
+++ b/client/src/components/Donation/patreon-button.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import {
+ DonationAmount,
donationUrls,
- patreonDefaultPledgeAmount,
PaymentProvider
} from '../../../../shared/config/donation-settings';
import envData from '../../../config/env.json';
@@ -14,21 +14,21 @@ const { patreonClientId }: { patreonClientId: string | null } = envData as {
interface PatreonButtonProps {
postPayment: (arg0: PostPayment) => void;
+ donationAmount: DonationAmount;
}
const PatreonButton = ({
- postPayment
+ postPayment,
+ donationAmount
}: PatreonButtonProps): JSX.Element | null => {
- if (
- !patreonClientId ||
- !patreonDefaultPledgeAmount ||
- !donationUrls.successUrl
- ) {
+ if (!patreonClientId || !donationAmount || !donationUrls.successUrl) {
return null;
}
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 redirectUri = `&redirect_uri=${donationUrls.successUrl}`;
const href = `https://www.patreon.com/oauth2/become-patron?response_type=code${pledgeLevel}${clientId}${redirectUri}${v2Params}`;
diff --git a/client/src/components/Donation/utils.ts b/client/src/components/Donation/utils.ts
new file mode 100644
index 00000000000..e2d4519c971
--- /dev/null
+++ b/client/src/components/Donation/utils.ts
@@ -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);
diff --git a/client/src/redux/types.ts b/client/src/redux/types.ts
index c1f0cbf7fa5..2a0177fb1e3 100644
--- a/client/src/redux/types.ts
+++ b/client/src/redux/types.ts
@@ -47,3 +47,14 @@ interface DefaultDonationFormState {
success: boolean;
error: null | string;
}
+
+export interface DonateFormState {
+ processing: boolean;
+ redirecting: boolean;
+ success: boolean;
+ error: string;
+ loading: {
+ stripe: boolean;
+ paypal: boolean;
+ };
+}
diff --git a/shared/config/donation-settings.ts b/shared/config/donation-settings.ts
index 3b08af927c1..b6e0ed31150 100644
--- a/shared/config/donation-settings.ts
+++ b/shared/config/donation-settings.ts
@@ -1,37 +1,21 @@
// 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 interface DonationConfig {
donationAmount: DonationAmount;
donationDuration: DonationDuration;
}
-export const durationsConfig: {
- month: 'monthly';
- onetime: 'one-time';
-} = {
- month: 'monthly',
- onetime: 'one-time'
-};
+export const subscriptionAmounts: DonationAmount[] = [500, 1000, 2000, 4000];
-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 = {
- donationAmount: defaultAmount.month,
- donationDuration: 'month'
-};
-export const modalDefaultDonation: DonationConfig = {
donationAmount: 500,
donationDuration: 'month'
};
+export const defaultTierAmount = 2000;
+
export const onetimeSKUConfig = {
live: [
{ amount: '15000', id: 'sku_IElisJHup0nojP' },
@@ -127,13 +111,6 @@ export const donationUrls = {
cancelUrl: 'https://freecodecamp.org/donate'
};
-export const patreonDefaultPledgeAmount = 500;
-
-export const aBTestConfig = {
- isTesting: true,
- type: 'secureIconButtonOnly'
-};
-
export enum PaymentContext {
Modal = 'modal',
DonatePage = 'donate page',