Add donation animation AB test (#53343)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
This commit is contained in:
Ahmad Abdolsaheb
2024-02-07 15:27:00 +03:00
committed by GitHub
parent c35c18fe4c
commit 7d4aacefd6
9 changed files with 518 additions and 104 deletions

View File

@@ -93,7 +93,9 @@
"link-account": "Link Account",
"unlink-account": "Unlink Account",
"update-card": "Update your card",
"donate-now": "Donate Now"
"donate-now": "Donate Now",
"confirm-amount": "Confirm amount",
"skip-advertisement": "Skip Advertisement"
},
"landing": {
"big-heading-1": "Learn to code — for free.",
@@ -578,6 +580,7 @@
"other-support": "If there is some other way you'd like to support our charity and its mission that isn't listed here, or if you have any questions at all, please email Quincy at quincy@freecodecamp.org.",
"bear-progress-alt": "Illustration of an adorable teddy bear with a pleading expression holding an empty money jar.",
"bear-completion-alt": "Illustration of an adorable teddy bear holding a large trophy.",
"flying-bear": "Illustration of an adorable teddy bear wearing a graduation cap and flying with a Supporter badge.",
"crucial-contribution": "Your contribution will be crucial in creating resources that empower millions of people to learn new skills and support their families.",
"support-benefits-title": "Benefits from becoming a Supporter:",
"support-benefits-1": "No more donation prompt popups",
@@ -598,7 +601,14 @@
"community-achivements-3": "Merged <0>2,753</0> code contributions into our open source repositories on GitHub",
"community-achivements-4": "Translated <0>2,106,203</0> words to make our curriculum and tutorials more accessible to speakers of many world languages",
"as-you-see": "As you can see, we're getting things done. So you can rest assured that we'll put your donations to good use.",
"get-benefits": "Get the benefits and the knowledge that youre helping our charity change education for the better. Become a Supporter today."
"get-benefits": "Get the benefits and the knowledge that youre helping our charity change education for the better. Become a supporter today.",
"modal-benefits-title": "Support us",
"help-us-more-certifications": "Help us build more certifications",
"remove-donation-popups": "Remove donation popups",
"help-millions-learn": "Help millions of people learn",
"reach-goals-faster": "Reach your goals faster",
"remove-distractions": "Remove distractions",
"animation-description": "This is a 20 second animated advertisement to encourage campers to become supporters of freeCodeCamp. The animation starts with a teddy bear who becomes a supporter. As a result, distracting pop-ups disappear and the bear gets to complete all of its goals. Then, it graduates and becomes an education super hero helping people around the world."
},
"report": {
"sign-in": "You need to be signed in to report a user",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 104 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,306 @@
import { Modal } from '@freecodecamp/react-bootstrap';
import { Col, Row } from '@freecodecamp/ui';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useFeature } from '@growthbook/growthbook-react';
import BearProgressModal from '../../assets/images/components/bear-progress-modal';
import BearBlockCompletion from '../../assets/images/components/bear-block-completion-modal';
import { closeDonationModal } from '../../redux/actions';
import { Spacer } from '../helpers';
import { PaymentContext } from '../../../../shared/config/donation-settings'; //
import donationAnimation from '../../assets/images/donation-bear-animation.svg';
import supporterBear from '../../assets/images/supporter-bear.svg';
import MultiTierDonationForm from './multi-tier-donation-form';
import { ModalBenefitList } from './donation-text-components';
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
type DonationModalBodyProps = {
activeDonors?: number;
closeDonationModal: typeof closeDonationModal;
recentlyClaimedBlock: RecentlyClaimedBlock;
executeGA: (arg: { event: string; action: string }) => void;
};
const Illustration = ({
recentlyClaimedBlock,
showAnimation
}: {
recentlyClaimedBlock: RecentlyClaimedBlock;
showAnimation?: boolean;
}) => {
const { t } = useTranslation();
if (showAnimation) {
return (
<img
alt={t('donate.flying-bear')}
id={'supporter-bear'}
src={supporterBear}
data-playwright-test-label='not-found-image'
/>
);
} else
return recentlyClaimedBlock ? (
<BearBlockCompletion className='donation-icon' />
) : (
<BearProgressModal className='donation-icon' />
);
};
function ModalHeader({
recentlyClaimedBlock,
showHeaderAndFooter,
donationAttempted,
showForm,
donationAnimationFlag
}: {
recentlyClaimedBlock: RecentlyClaimedBlock;
showHeaderAndFooter: boolean;
donationAttempted: boolean;
showForm: boolean;
donationAnimationFlag: boolean;
}) {
const { t } = useTranslation();
if (!showHeaderAndFooter || donationAttempted) {
return null;
} else if (!donationAnimationFlag) {
return (
<Row className='text-center block-modal-text'>
<Col sm={10} smOffset={1} xs={12}>
{recentlyClaimedBlock !== null && (
<b>
{t('donate.nicely-done', {
block: t(
`intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
)
})}
</b>
)}
<h2>{t('donate.help-us-develop')}</h2>
</Col>
</Row>
);
} else if (!showForm) {
return (
<Row className='text-center block-modal-text'>
<Col sm={10} smOffset={1} xs={12}>
<h2>{t('donate.modal-benefits-title')}</h2>
</Col>
</Row>
);
} else {
return null;
}
}
function CloseButtonRow({
donationAttempted,
closeDonationModal
}: {
donationAttempted: boolean;
closeDonationModal: () => void;
}) {
const { t } = useTranslation();
return (
<Row>
<Col sm={4} smOffset={4} xs={8} xsOffset={2}>
<button
className='close-button'
type='button'
onClick={closeDonationModal}
>
{donationAttempted ? t('buttons.close') : t('buttons.ask-later')}
</button>
</Col>
</Row>
);
}
const Benefits = ({
setShowForm,
executeGA
}: {
setShowForm: (arg: boolean) => void;
executeGA: (arg: { event: string; action: string }) => void;
}) => {
const { t } = useTranslation();
const handleBecomeSupporterClick = () => {
executeGA({
event: 'donation_related',
action: `Modal Become Supporter Click`
});
setShowForm(true);
};
return (
<Row className={'donate-btn-group'}>
<Col xs={12}>
<ModalBenefitList />
<Spacer size='small' />
<button
className='text-center confirm-donation-btn donate-btn-group'
type='submit'
onClick={handleBecomeSupporterClick}
>
{t('donate.become-supporter')}
</button>
<Spacer size='medium' />
</Col>
</Row>
);
};
const AnimationContainer = ({
setIsAnimationVisible
}: {
setIsAnimationVisible: (arg: boolean) => void;
}) => {
const { t } = useTranslation();
return (
<>
<p style={{ opacity: 0, position: 'absolute' }}>
{t('donate.animation-description')}{' '}
</p>
<div className='donation-animation-container' aria-hidden='true'>
<div className='donation-animation-bullet-points'>
<p className='donation-animation-bullet-1'>
{t('donate.become-supporter')}
</p>
<p className='donation-animation-bullet-2'>
{t('donate.remove-distractions')}
</p>
<p className='donation-animation-bullet-3'>
{t('donate.reach-goals-faster')}
</p>
<p className='donation-animation-bullet-4'>
{t('donate.help-millions-learn')}
</p>
</div>
<img
alt=''
src={donationAnimation}
id={'donation-animation'}
data-playwright-test-label='not-found-image'
/>
</div>
<button
style={{ opacity: 0, position: 'absolute' }}
className={'sr-only'}
onClick={() => setIsAnimationVisible(false)}
>
{t('buttons.skip-advertisement')}
</button>
</>
);
};
const BecomeASupporterConfirmation = ({
donationAnimationFlag,
recentlyClaimedBlock,
showHeaderAndFooter,
closeDonationModal,
donationAttempted,
showForm,
setShowHeaderAndFooter,
handleProcessing,
setShowForm,
executeGA
}: {
donationAnimationFlag: boolean;
recentlyClaimedBlock: RecentlyClaimedBlock;
showHeaderAndFooter: boolean;
closeDonationModal: () => void;
donationAttempted: boolean;
showForm: boolean;
setShowHeaderAndFooter: (arg: boolean) => void;
handleProcessing: () => void;
setShowForm: (arg: boolean) => void;
executeGA: (arg: { event: string; action: string }) => void;
}) => {
return (
<div className='no-delay-fade-in'>
<div className='donation-icon-container'>
<Illustration
showAnimation={donationAnimationFlag}
recentlyClaimedBlock={recentlyClaimedBlock}
/>
</div>
<ModalHeader
recentlyClaimedBlock={recentlyClaimedBlock}
showHeaderAndFooter={showHeaderAndFooter}
donationAttempted={donationAttempted}
showForm={showForm}
donationAnimationFlag={donationAnimationFlag}
/>
<Spacer size='small' />
{showForm || !donationAnimationFlag ? (
<MultiTierDonationForm
setShowHeaderAndFooter={setShowHeaderAndFooter}
handleProcessing={handleProcessing}
paymentContext={PaymentContext.Modal}
isMinimalForm={true}
isAnimationEnabled={donationAnimationFlag}
/>
) : (
<Benefits setShowForm={setShowForm} executeGA={executeGA} />
)}
{(showHeaderAndFooter || donationAttempted) && (
<CloseButtonRow
closeDonationModal={closeDonationModal}
donationAttempted={donationAttempted}
/>
)}
</div>
);
};
function DonationModalBody({
closeDonationModal,
recentlyClaimedBlock,
executeGA
}: DonationModalBodyProps): JSX.Element {
const [donationAttempted, setDonationAttempted] = useState(false);
const [showHeaderAndFooter, setShowHeaderAndFooter] = useState(true);
const donationAnimationFlag = useFeature('donation-animation').on;
const [isAnimationVisible, setIsAnimationVisible] = useState(true);
const [showForm, setShowForm] = useState(false);
const handleProcessing = () => {
setDonationAttempted(true);
};
useEffect(() => {
const timer = setTimeout(() => {
setIsAnimationVisible(false);
}, 20000); // 20000 milliseconds = 20 seconds
// Clear the timer if the component unmounts
return () => clearTimeout(timer);
}, []);
return (
<Modal.Body>
<div aria-live={'polite'}>
{donationAnimationFlag && isAnimationVisible ? (
<AnimationContainer setIsAnimationVisible={setIsAnimationVisible} />
) : (
<BecomeASupporterConfirmation
donationAnimationFlag={donationAnimationFlag}
recentlyClaimedBlock={recentlyClaimedBlock}
showHeaderAndFooter={showHeaderAndFooter}
closeDonationModal={closeDonationModal}
donationAttempted={donationAttempted}
showForm={showForm}
setShowHeaderAndFooter={setShowHeaderAndFooter}
handleProcessing={handleProcessing}
setShowForm={setShowForm}
executeGA={executeGA}
/>
)}
</div>
</Modal.Body>
);
}
DonationModalBody.displayName = 'DonationModalBody';
export default DonationModalBody;

View File

@@ -1,15 +1,11 @@
import { Modal } from '@freecodecamp/react-bootstrap';
import { Col, Row } from '@freecodecamp/ui';
import { WindowLocation } from '@reach/router';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { goToAnchor } from 'react-scrollable-anchor';
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
import { createSelector } from 'reselect';
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,
@@ -17,9 +13,7 @@ import {
} from '../../redux/selectors';
import { isLocationSuperBlock } from '../../utils/path-parsers';
import { playTone } from '../../utils/tone';
import { Spacer } from '../helpers';
import { PaymentContext } from '../../../../shared/config/donation-settings'; //
import MultiTierDonationForm from './multi-tier-donation-form';
import DonationModalBody from './donation-modal-body';
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
@@ -50,65 +44,6 @@ type DonateModalProps = {
show: boolean;
};
const Illustration = ({
recentlyClaimedBlock
}: {
recentlyClaimedBlock: RecentlyClaimedBlock;
}) => {
return recentlyClaimedBlock ? (
<BearBlockCompletion className='donation-icon' />
) : (
<BearProgressModal className='donation-icon' />
);
};
function ModalHeader({
recentlyClaimedBlock
}: {
recentlyClaimedBlock: RecentlyClaimedBlock;
}) {
const { t } = useTranslation();
return (
<Row className='text-center block-modal-text'>
<Col sm={10} smOffset={1} xs={12}>
{recentlyClaimedBlock !== null && (
<b>
{t('donate.nicely-done', {
block: t(
`intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
)
})}
</b>
)}
<h2>{t('donate.help-us-develop')}</h2>
</Col>
</Row>
);
}
function CloseButtonRow({
donationAttempted,
closeDonationModal
}: {
donationAttempted: boolean;
closeDonationModal: () => void;
}) {
const { t } = useTranslation();
return (
<Row>
<Col sm={4} smOffset={4} xs={8} xsOffset={2}>
<button
className='close-button'
type='button'
onClick={closeDonationModal}
>
{donationAttempted ? t('buttons.close') : t('buttons.ask-later')}
</button>
</Col>
</Row>
);
}
function DonateModal({
show,
closeDonationModal,
@@ -116,13 +51,6 @@ function DonateModal({
location,
recentlyClaimedBlock
}: DonateModalProps): JSX.Element {
const [donationAttempted, setDonationAttempted] = useState(false);
const [showHeaderAndFooter, setShowHeaderAndFooter] = useState(true);
const handleProcessing = () => {
setDonationAttempted(true);
};
useEffect(() => {
if (show) {
void playTone('donation');
@@ -149,28 +77,13 @@ function DonateModal({
className='donation-modal'
onExited={handleModalHide}
show={show}
data-cy='donation-modal'
>
<Modal.Body className='no-delay-fade-in'>
<div className='donation-icon-container'>
<Illustration recentlyClaimedBlock={recentlyClaimedBlock} />
</div>
{showHeaderAndFooter && !donationAttempted && (
<ModalHeader recentlyClaimedBlock={recentlyClaimedBlock} />
)}
<Spacer size='small' />
<MultiTierDonationForm
setShowHeaderAndFooter={setShowHeaderAndFooter}
handleProcessing={handleProcessing}
paymentContext={PaymentContext.Modal}
isMinimalForm={true}
/>
{(showHeaderAndFooter || donationAttempted) && (
<CloseButtonRow
closeDonationModal={closeDonationModal}
donationAttempted={donationAttempted}
/>
)}
</Modal.Body>
<DonationModalBody
closeDonationModal={closeDonationModal}
recentlyClaimedBlock={recentlyClaimedBlock}
executeGA={executeGA}
/>
</Modal>
);
}

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import Caret from '../../assets/icons/caret';
import { Spacer } from '../helpers';
import GreenPass from '../../assets/icons/green-pass';
const POBOX = (
<>
@@ -225,7 +226,7 @@ export const SupportBenefitsText = ({
);
};
const BenefitsList = (): JSX.Element => {
export const BenefitsList = (): JSX.Element => {
const { t } = useTranslation();
return (
<ul>
@@ -317,3 +318,23 @@ export const GetSupporterBenefitsText = ({
</>
);
};
export const ModalBenefitList = () => {
const { t } = useTranslation();
return (
<ul>
<li>
<GreenPass aria-disabled={true} />
{t('donate.help-us-more-certifications')}
</li>
<li>
<GreenPass aria-disabled={true} />
{t('donate.remove-donation-popups')}
</li>
<li>
<GreenPass aria-disabled={true} />
{t('donate.help-millions-learn')}
</li>
</ul>
);
};

View File

@@ -273,6 +273,19 @@ li.disabled > a {
.donation-modal p {
font-size: 0.9rem;
}
.donation-modal ul {
padding: 0;
}
.donation-modal li {
list-style-type: none;
margin-bottom: 4px;
}
.donation-modal li svg {
margin: 0 5px;
}
.donation-label-modal {
font-weight: normal;
text-align: center;
@@ -736,3 +749,139 @@ a.patreon-button:hover {
height: 6em;
width: auto;
}
#supporter-bear {
height: 250px;
}
.donation-animation-container,
.donation-animation-bullet-points {
position: relative;
width: 100%;
}
.donation-animation-bullet-points p {
position: absolute;
color: white;
text-align: center;
width: 100%;
font-size: 200%;
padding: 0 20px;
}
.donation-animation-bullet-1 {
animation-name: animation-bullet-1;
}
.donation-animation-bullet-2 {
animation-name: animation-bullet-2;
}
.donation-animation-bullet-3 {
animation-name: animation-bullet-3;
}
.donation-animation-bullet-4 {
animation-name: animation-bullet-4;
}
.donation-animation-bullet-1,
.donation-animation-bullet-2,
.donation-animation-bullet-3,
.donation-animation-bullet-4 {
opacity: 0;
animation-fill-mode: forwards;
animation-duration: 20s;
margin-top: 40px;
animation-timing-function: normal;
}
@keyframes animation-bullet-1 {
1% {
opacity: 0;
margin-top: 40px;
}
2.5% {
opacity: 1;
margin-top: 80px;
}
19.5% {
opacity: 1;
margin-top: 80px;
}
21% {
opacity: 0;
margin-top: 120px;
}
}
@keyframes animation-bullet-2 {
21% {
opacity: 0;
margin-top: 40px;
}
22.5% {
opacity: 1;
margin-top: 80px;
}
38.5% {
opacity: 1;
margin-top: 80px;
}
40% {
opacity: 0;
margin-top: 120px;
}
}
@keyframes animation-bullet-3 {
40% {
opacity: 0;
margin-top: 40px;
}
41.5% {
opacity: 1;
margin-top: 80px;
}
64% {
opacity: 1;
margin-top: 80px;
}
65.5% {
opacity: 0;
margin-top: 120px;
}
}
@keyframes animation-bullet-4 {
65.5% {
opacity: 0;
margin-top: 40px;
}
67% {
opacity: 1;
margin-top: 80px;
}
96% {
opacity: 1;
margin-top: 80px;
}
97.5% {
opacity: 0;
}
}
.tester-text {
margin-top: 80px;
}
@media (max-width: 768px) {
.donation-animation-bullet-points p {
font-size: 150%;
}
#donation-animation {
object-fit: cover;
min-height: 600px;
width: 100%;
}
}

View File

@@ -27,15 +27,18 @@ type MultiTierDonationFormProps = {
paymentContext: PaymentContext;
isMinimalForm?: boolean;
defaultTheme?: Themes;
isAnimationEnabled?: boolean;
};
function SelectionTabs({
donationAmount,
setDonationAmount,
setShowDonateForm
setShowDonateForm,
isAnimationEnabled
}: {
donationAmount: DonationAmount;
setDonationAmount: React.Dispatch<React.SetStateAction<DonationAmount>>;
setShowDonateForm: React.Dispatch<React.SetStateAction<boolean>>;
isAnimationEnabled?: boolean;
}) {
const { t } = useTranslation();
const switchTab = (value: string): void => {
@@ -97,7 +100,9 @@ function SelectionTabs({
data-cy='donation-tier-selection-button'
onClick={() => setShowDonateForm(true)}
>
{t('buttons.donate')}
{isAnimationEnabled
? t('buttons.confirm-amount')
: t('buttons.donate')}
</button>
<Spacer size='medium' />
</Col>
@@ -138,7 +143,8 @@ const MultiTierDonationForm: React.FC<MultiTierDonationFormProps> = ({
handleProcessing,
setShowHeaderAndFooter,
isMinimalForm,
paymentContext
paymentContext,
isAnimationEnabled
}) => {
const [donationAmount, setDonationAmount] = useState(defaultTierAmount);
@@ -155,6 +161,7 @@ const MultiTierDonationForm: React.FC<MultiTierDonationFormProps> = ({
donationAmount={donationAmount}
setDonationAmount={setDonationAmount}
setShowDonateForm={setShowDonateForm}
isAnimationEnabled={isAnimationEnabled}
/>
</div>
<div {...(!showDonateForm && { className: 'hide' })}>

View File

@@ -34,8 +34,6 @@ describe('Donate page', () => {
projects.forEach(project => submitProject(project));
// pop up modal
cy.contains(
'Nicely done. You just completed Front End Development Libraries Projects.'
);
cy.get("[data-cy='donation-modal']");
});
});