Skip to content

Commit 07e7088

Browse files
huyenltnguyenahmaxedojeytonwilliamsmoT01
authored
fix(client): show donation modal on module completion (freeCodeCamp#57583)
Co-authored-by: ahmad abdolsaheb <[email protected]> Co-authored-by: Oliver Eyton-Williams <[email protected]> Co-authored-by: Tom <[email protected]>
1 parent 118b8d2 commit 07e7088

File tree

20 files changed

+328
-80
lines changed

20 files changed

+328
-80
lines changed

client/gatsby-node.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ exports.createPages = async function createPages({
112112
superOrder
113113
template
114114
usesMultifileEditor
115+
chapter
116+
module
115117
}
116118
}
117119
}

client/src/components/Donation/donation-modal-body.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@ import { useTranslation } from 'react-i18next';
33
import { useFeature } from '@growthbook/growthbook-react';
44
import { Col, Row, Modal, Spacer } from '@freecodecamp/ui';
55
import { closeDonationModal } from '../../redux/actions';
6-
import { PaymentContext } from '../../../../shared/config/donation-settings'; //
6+
import { PaymentContext } from '../../../../shared/config/donation-settings';
77
import donationAnimation from '../../assets/images/donation-bear-animation.svg';
88
import donationAnimationB from '../../assets/images/new-bear-animation.svg';
99
import supporterBear from '../../assets/images/supporter-bear.svg';
1010
import callGA from '../../analytics/call-ga';
1111
import MultiTierDonationForm from './multi-tier-donation-form';
1212
import { ModalBenefitList } from './donation-text-components';
13-
14-
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
13+
import { DonatableSectionRecentlyCompleted } from './types';
1514

1615
type DonationModalBodyProps = {
1716
activeDonors?: number;
1817
closeDonationModal: typeof closeDonationModal;
19-
recentlyClaimedBlock: RecentlyClaimedBlock;
18+
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
2019
setCanClose: (canClose: boolean) => void;
2120
};
2221

@@ -32,37 +31,39 @@ const Illustration = () => {
3231
};
3332

3433
function ModalHeader({
35-
recentlyClaimedBlock,
3634
showHeaderAndFooter,
3735
donationAttempted,
38-
showForm
36+
showForm,
37+
donatableSectionRecentlyCompleted
3938
}: {
40-
recentlyClaimedBlock: RecentlyClaimedBlock;
39+
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
4140
showHeaderAndFooter: boolean;
4241
donationAttempted: boolean;
4342
showForm: boolean;
4443
}) {
4544
const { t } = useTranslation();
45+
const { section, superBlock, title } =
46+
donatableSectionRecentlyCompleted || {};
4647

4748
if (!showHeaderAndFooter || donationAttempted) {
4849
return null;
4950
} else if (!showForm) {
5051
return (
5152
<Row className='text-center'>
5253
<Col sm={10} smOffset={1} xs={12}>
53-
{recentlyClaimedBlock !== null && (
54+
{donatableSectionRecentlyCompleted && (
5455
<>
5556
<b>
5657
{t('donate.nicely-done', {
57-
block: t(
58-
`intro:${recentlyClaimedBlock.superBlock}.blocks.${recentlyClaimedBlock.block}.title`
59-
)
58+
block:
59+
section === 'module'
60+
? t(`intro:${superBlock}.${section}s.${title}`)
61+
: t(`intro:${superBlock}.${section}s.${title}.title`)
6062
})}
6163
</b>
6264
<Spacer size='xs' />
6365
</>
6466
)}
65-
6667
<Modal.Header showCloseButton={false} borderless>
6768
{t('donate.modal-benefits-title')}
6869
</Modal.Header>
@@ -188,16 +189,16 @@ const AnimationContainer = ({
188189
};
189190

190191
const BecomeASupporterConfirmation = ({
191-
recentlyClaimedBlock,
192192
showHeaderAndFooter,
193193
closeDonationModal,
194194
donationAttempted,
195195
showForm,
196196
setShowHeaderAndFooter,
197197
handleProcessing,
198-
setShowForm
198+
setShowForm,
199+
donatableSectionRecentlyCompleted
199200
}: {
200-
recentlyClaimedBlock: RecentlyClaimedBlock;
201+
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
201202
showHeaderAndFooter: boolean;
202203
closeDonationModal: () => void;
203204
donationAttempted: boolean;
@@ -212,7 +213,7 @@ const BecomeASupporterConfirmation = ({
212213
<Illustration />
213214
</div>
214215
<ModalHeader
215-
recentlyClaimedBlock={recentlyClaimedBlock}
216+
donatableSectionRecentlyCompleted={donatableSectionRecentlyCompleted}
216217
showHeaderAndFooter={showHeaderAndFooter}
217218
donationAttempted={donationAttempted}
218219
showForm={showForm}
@@ -240,7 +241,7 @@ const BecomeASupporterConfirmation = ({
240241

241242
function DonationModalBody({
242243
closeDonationModal,
243-
recentlyClaimedBlock,
244+
donatableSectionRecentlyCompleted,
244245
setCanClose
245246
}: DonationModalBodyProps): JSX.Element {
246247
const [donationAttempted, setDonationAttempted] = useState(false);
@@ -273,7 +274,9 @@ function DonationModalBody({
273274
<AnimationContainer secondsRemaining={secondsRemaining} />
274275
) : (
275276
<BecomeASupporterConfirmation
276-
recentlyClaimedBlock={recentlyClaimedBlock}
277+
donatableSectionRecentlyCompleted={
278+
donatableSectionRecentlyCompleted
279+
}
277280
showHeaderAndFooter={showHeaderAndFooter}
278281
closeDonationModal={closeDonationModal}
279282
donationAttempted={donationAttempted}

client/src/components/Donation/donation-modal.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@ import { useFeature } from '@growthbook/growthbook-react';
1010
import { closeDonationModal } from '../../redux/actions';
1111
import {
1212
isDonationModalOpenSelector,
13-
recentlyClaimedBlockSelector
13+
donatableSectionRecentlyCompletedSelector
1414
} from '../../redux/selectors';
15+
1516
import { isLocationSuperBlock } from '../../utils/path-parsers';
1617
import { playTone } from '../../utils/tone';
1718
import callGA from '../../analytics/call-ga';
19+
import { DonatableSectionRecentlyCompleted } from './types';
1820
import DonationModalBody from './donation-modal-body';
1921

20-
type RecentlyClaimedBlock = null | { block: string; superBlock: string };
21-
2222
const mapStateToProps = createSelector(
2323
isDonationModalOpenSelector,
24-
recentlyClaimedBlockSelector,
25-
(show: boolean, recentlyClaimedBlock: RecentlyClaimedBlock) => ({
24+
donatableSectionRecentlyCompletedSelector,
25+
(
26+
show: boolean,
27+
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted
28+
) => ({
2629
show,
27-
recentlyClaimedBlock
30+
donatableSectionRecentlyCompleted
2831
})
2932
);
3033

@@ -35,15 +38,15 @@ type DonateModalProps = {
3538
activeDonors?: number;
3639
closeDonationModal: typeof closeDonationModal;
3740
location?: WindowLocation;
38-
recentlyClaimedBlock: RecentlyClaimedBlock;
41+
donatableSectionRecentlyCompleted: DonatableSectionRecentlyCompleted;
3942
show: boolean;
4043
};
4144

4245
function DonateModal({
4346
show,
4447
closeDonationModal,
4548
location,
46-
recentlyClaimedBlock
49+
donatableSectionRecentlyCompleted
4750
}: DonateModalProps): JSX.Element {
4851
const [canClose, setCanClose] = useState(false);
4952
const isA11yFeatureEnabled = useFeature('a11y-donation-modal').on;
@@ -55,11 +58,11 @@ function DonateModal({
5558
callGA({
5659
event: 'donation_view',
5760
action: `Displayed ${
58-
recentlyClaimedBlock !== null ? 'Block' : 'Progress'
61+
donatableSectionRecentlyCompleted !== null ? 'Block' : 'Progress'
5962
} Donation Modal`
6063
});
6164
}
62-
}, [show, recentlyClaimedBlock]);
65+
}, [show, donatableSectionRecentlyCompleted]);
6366

6467
const handleModalHide = () => {
6568
// If modal is open on a SuperBlock page
@@ -76,7 +79,7 @@ function DonateModal({
7679
<Modal size='large' onClose={handleModalHide} open={show}>
7780
<DonationModalBody
7881
closeDonationModal={closeDonationModal}
79-
recentlyClaimedBlock={recentlyClaimedBlock}
82+
donatableSectionRecentlyCompleted={donatableSectionRecentlyCompleted}
8083
setCanClose={setCanClose}
8184
/>
8285
</Modal>

client/src/components/Donation/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { PaymentIntentResult } from '@stripe/stripe-js';
2+
import { SuperBlocks } from '../../../../shared/config/curriculum';
23

34
export type PaymentContext = 'modal' | 'donate page' | 'certificate';
45
export type PaymentProvider = 'patreon' | 'paypal' | 'stripe' | 'stripe card';
@@ -28,3 +29,9 @@ export interface DonationApprovalData {
2829
paypal: boolean;
2930
};
3031
}
32+
33+
export type DonatableSectionRecentlyCompleted = null | {
34+
section: string;
35+
title: string;
36+
superBlock: SuperBlocks;
37+
};

client/src/components/Map/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import './map.css';
2121

2222
import {
2323
isSignedInSelector,
24-
currentCertsSelector
24+
currentCertsSelector,
25+
completedChallengesIdsSelector
2526
} from '../../redux/selectors';
2627

2728
import { RibbonIcon } from '../../assets/icons/completion-ribbon';
@@ -31,7 +32,6 @@ import {
3132
certSlugTypeMap,
3233
superBlockCertTypeMap
3334
} from '../../../../shared/config/certification-settings';
34-
import { completedChallengesIdsSelector } from '../../templates/Challenges/redux/selectors';
3535

3636
interface MapProps {
3737
forLanding?: boolean;

client/src/redux/action-types.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ export const actionTypes = createTypes(
99
'toggleTheme',
1010
'appMount',
1111
'hardGoTo',
12-
'allowBlockDonationRequests',
12+
'allowSectionDonationRequests',
1313
'setRenderStartTime',
14-
'preventBlockDonationRequests',
14+
'preventSectionDonationRequests',
1515
'setIsRandomCompletionThreshold',
1616
'openDonationModal',
1717
'closeDonationModal',

client/src/redux/actions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ export const tryToShowDonationModal = createAction(
88
actionTypes.tryToShowDonationModal
99
);
1010

11-
export const allowBlockDonationRequests = createAction(
12-
actionTypes.allowBlockDonationRequests
11+
export const allowSectionDonationRequests = createAction(
12+
actionTypes.allowSectionDonationRequests
1313
);
1414
export const setRenderStartTime = createAction(actionTypes.setRenderStartTime);
1515
export const closeDonationModal = createAction(actionTypes.closeDonationModal);
1616
export const openDonationModal = createAction(actionTypes.openDonationModal);
17-
export const preventBlockDonationRequests = createAction(
18-
actionTypes.preventBlockDonationRequests
17+
export const preventSectionDonationRequests = createAction(
18+
actionTypes.preventSectionDonationRequests
1919
);
2020
export const setIsRandomCompletionThreshold = createAction(
2121
actionTypes.setIsRandomCompletionThreshold

client/src/redux/donation-saga.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,20 @@ import {
2323
getSessionChallengeData,
2424
saveCurrentCount
2525
} from '../utils/session-storage';
26+
2627
import { actionTypes as appTypes } from './action-types';
2728
import {
2829
openDonationModal,
2930
postChargeComplete,
3031
postChargeProcessing,
3132
postChargeError,
32-
preventBlockDonationRequests,
33+
preventSectionDonationRequests,
3334
updateCardError,
3435
updateCardRedirecting
3536
} from './actions';
3637
import {
3738
isDonatingSelector,
38-
recentlyClaimedBlockSelector,
39+
donatableSectionRecentlyCompletedSelector,
3940
shouldRequestDonationSelector,
4041
isSignedInSelector,
4142
completedChallengesSelector
@@ -46,27 +47,31 @@ const updateCardErrorMessage = i18next.t('donate.error-3');
4647

4748
function* showDonateModalSaga() {
4849
let shouldRequestDonation = yield select(shouldRequestDonationSelector);
49-
const recentlyClaimedBlock = yield select(recentlyClaimedBlockSelector);
5050
const MODAL_SHOWN_KEY = 'modalShownTimestamp';
5151
const modalShownTimestamp = sessionStorage.getItem(MODAL_SHOWN_KEY);
52-
const isModalRecentlyShown = Date.now() - modalShownTimestamp < 20000;
53-
if (
54-
shouldRequestDonation &&
55-
recentlyClaimedBlock &&
56-
chapterBasedSuperBlocks.includes(recentlyClaimedBlock.superBlock)
57-
) {
58-
yield put(preventBlockDonationRequests());
59-
} else if (shouldRequestDonation || isModalRecentlyShown) {
52+
// If the modal has been shown in the last 20 seconds, the animation should
53+
// still be running:
54+
const isAnimationRunning = Date.now() - modalShownTimestamp < 20000;
55+
const shouldShowModal = shouldRequestDonation || isAnimationRunning;
56+
const donatableSectionRecentlyCompleted = yield select(
57+
donatableSectionRecentlyCompletedSelector
58+
);
59+
60+
if (shouldShowModal) {
6061
yield delay(200);
6162
yield put(openDonationModal());
6263
sessionStorage.setItem(MODAL_SHOWN_KEY, Date.now());
6364
yield take(appTypes.closeDonationModal);
64-
if (recentlyClaimedBlock) {
65-
yield put(preventBlockDonationRequests());
66-
} else {
65+
if (!donatableSectionRecentlyCompleted) {
6766
yield call(saveCurrentCount);
6867
}
6968
}
69+
70+
/* users can complete donatable section but have less than 10 completed challenge
71+
to show the donation modal.*/
72+
if (donatableSectionRecentlyCompleted) {
73+
yield put(preventSectionDonationRequests());
74+
}
7075
}
7176

7277
export function* postChargeSaga({

client/src/redux/index.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const defaultDonationFormState = {
5252
const initialState = {
5353
appUsername: '',
5454
isRandomCompletionThreshold: false,
55-
recentlyClaimedBlock: null,
55+
donatableSectionRecentlyCompleted: null,
5656
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
5757
examInProgress: false,
5858
isProcessing: false,
@@ -138,10 +138,14 @@ export const reducer = handleActions(
138138
}
139139
};
140140
},
141-
[actionTypes.allowBlockDonationRequests]: (state, { payload }) => {
141+
[actionTypes.allowSectionDonationRequests]: (state, { payload }) => {
142142
return {
143143
...state,
144-
recentlyClaimedBlock: payload
144+
donatableSectionRecentlyCompleted: {
145+
block: payload.block,
146+
module: payload.module,
147+
superBlock: payload.superBlock
148+
}
145149
};
146150
},
147151
[actionTypes.setRenderStartTime]: (state, { payload }) => {
@@ -277,9 +281,9 @@ export const reducer = handleActions(
277281
...state,
278282
showDonationModal: true
279283
}),
280-
[actionTypes.preventBlockDonationRequests]: state => ({
284+
[actionTypes.preventSectionDonationRequests]: state => ({
281285
...state,
282-
recentlyClaimedBlock: null
286+
donatableSectionRecentlyCompleted: null
283287
}),
284288
[actionTypes.setIsRandomCompletionThreshold]: (state, { payload }) => ({
285289
...state,

client/src/redux/prop-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ export type ChallengeMeta = {
391391
superBlock: SuperBlocks;
392392
title?: string;
393393
challengeType?: number;
394+
blockType?: BlockTypes;
394395
helpCategory: string;
395396
disableLoopProtectTests: boolean;
396397
disableLoopProtectPreview: boolean;

0 commit comments

Comments
 (0)