mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
feat(client): AB test displaying the progress donation modal multiple times (#50408)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
co-authored by
Oliver Eyton-Williams
parent
65239ee68b
commit
76673d544b
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
|
||||
import { useFeature } from '@growthbook/growthbook-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
isSignedInSelector,
|
||||
showMultipleProgressModalsSelector
|
||||
} from '../../redux/selectors';
|
||||
import { setShowMultipleProgressModals } from '../../redux/actions';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
showMultipleProgressModalsSelector,
|
||||
(isSignedIn, showMultipleProgressModals: boolean) => ({
|
||||
isSignedIn,
|
||||
showMultipleProgressModals
|
||||
})
|
||||
);
|
||||
|
||||
type StateProps = ReturnType<typeof mapStateToProps>;
|
||||
type DispatchProps = { setShowMultipleProgressModals: (arg: boolean) => void };
|
||||
|
||||
interface GrowthBookReduxConnector extends StateProps, DispatchProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setShowMultipleProgressModals
|
||||
};
|
||||
|
||||
const GrowthBookReduxConnector = ({
|
||||
children,
|
||||
isSignedIn,
|
||||
showMultipleProgressModals,
|
||||
setShowMultipleProgressModals
|
||||
}: GrowthBookReduxConnector) => {
|
||||
const displayProgressModalMultipleTimes = useFeature(
|
||||
'display_progress_modal_multiple_times'
|
||||
).on;
|
||||
useEffect(() => {
|
||||
if (
|
||||
isSignedIn &&
|
||||
displayProgressModalMultipleTimes &&
|
||||
!showMultipleProgressModals
|
||||
) {
|
||||
setShowMultipleProgressModals(true);
|
||||
}
|
||||
}, [
|
||||
isSignedIn,
|
||||
showMultipleProgressModals,
|
||||
displayProgressModalMultipleTimes,
|
||||
setShowMultipleProgressModals
|
||||
]);
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(GrowthBookReduxConnector);
|
||||
@@ -11,6 +11,7 @@ import { createSelector } from 'reselect';
|
||||
import { isSignedInSelector, userSelector } from '../../redux/selectors';
|
||||
import envData from '../../../../config/env.json';
|
||||
import { User } from '../../redux/prop-types';
|
||||
import GrowthBookReduxConnector from './growth-book-redux-connector';
|
||||
|
||||
const { clientLocale, growthbookUri } = envData as {
|
||||
clientLocale: string;
|
||||
@@ -85,7 +86,9 @@ const GrowthBookWrapper = ({
|
||||
}, [isSignedIn, user.email, user.joinDate, user.completedChallenges]);
|
||||
|
||||
return (
|
||||
<GrowthBookProvider growthbook={growthbook}>{children}</GrowthBookProvider>
|
||||
<GrowthBookProvider growthbook={growthbook}>
|
||||
<GrowthBookReduxConnector>{children}</GrowthBookReduxConnector>
|
||||
</GrowthBookProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ export const actionTypes = createTypes(
|
||||
'setRenderStartTime',
|
||||
'hideCodeAlly',
|
||||
'preventBlockDonationRequests',
|
||||
'preventProgressDonationRequests',
|
||||
'setCompletionCountWhenShownProgressModal',
|
||||
'setShowMultipleProgressModals',
|
||||
'openDonationModal',
|
||||
'closeDonationModal',
|
||||
'openSignoutModal',
|
||||
|
||||
@@ -19,8 +19,11 @@ export const openDonationModal = createAction(actionTypes.openDonationModal);
|
||||
export const preventBlockDonationRequests = createAction(
|
||||
actionTypes.preventBlockDonationRequests
|
||||
);
|
||||
export const preventProgressDonationRequests = createAction(
|
||||
actionTypes.preventProgressDonationRequests
|
||||
export const setCompletionCountWhenShownProgressModal = createAction(
|
||||
actionTypes.setCompletionCountWhenShownProgressModal
|
||||
);
|
||||
export const setShowMultipleProgressModals = createAction(
|
||||
actionTypes.setShowMultipleProgressModals
|
||||
);
|
||||
export const updateDonationFormState = createAction(
|
||||
actionTypes.updateDonationFormState
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
postChargeProcessing,
|
||||
postChargeError,
|
||||
preventBlockDonationRequests,
|
||||
preventProgressDonationRequests,
|
||||
setCompletionCountWhenShownProgressModal,
|
||||
executeGA
|
||||
} from './actions';
|
||||
import {
|
||||
@@ -45,7 +45,7 @@ function* showDonateModalSaga() {
|
||||
if (recentlyClaimedBlock) {
|
||||
yield put(preventBlockDonationRequests());
|
||||
} else {
|
||||
yield put(preventProgressDonationRequests());
|
||||
yield put(setCompletionCountWhenShownProgressModal());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { createGaSaga } from './ga-saga';
|
||||
import hardGoToEpic from './hard-go-to-epic';
|
||||
import { createReportUserSaga } from './report-user-saga';
|
||||
import { createSaveChallengeSaga } from './save-challenge-saga';
|
||||
import { savedChallengesSelector } from './selectors';
|
||||
import { completionCountSelector, savedChallengesSelector } from './selectors';
|
||||
import { actionTypes as settingsTypes } from './settings/action-types';
|
||||
import { createShowCertSaga } from './show-cert-saga';
|
||||
import updateCompleteEpic from './update-complete-epic';
|
||||
@@ -43,8 +43,10 @@ export const defaultDonationFormState = {
|
||||
|
||||
const initialState = {
|
||||
appUsername: '',
|
||||
showMultipleProgressModals: false,
|
||||
recentlyClaimedBlock: null,
|
||||
canRequestProgressDonation: true,
|
||||
completionCountWhenShownProgressModal: 0,
|
||||
progressDonationModalShown: false,
|
||||
completionCount: 0,
|
||||
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
||||
examInProgress: false,
|
||||
@@ -251,9 +253,16 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
recentlyClaimedBlock: null
|
||||
}),
|
||||
[actionTypes.preventProgressDonationRequests]: state => ({
|
||||
[actionTypes.setCompletionCountWhenShownProgressModal]: state => ({
|
||||
...state,
|
||||
canRequestProgressDonation: false
|
||||
progressDonationModalShown: true,
|
||||
completionCountWhenShownProgressModal: completionCountSelector({
|
||||
[MainApp]: state
|
||||
})
|
||||
}),
|
||||
[actionTypes.setShowMultipleProgressModals]: (state, { payload }) => ({
|
||||
...state,
|
||||
showMultipleProgressModals: payload
|
||||
}),
|
||||
[actionTypes.resetUserData]: state => ({
|
||||
...state,
|
||||
|
||||
@@ -7,10 +7,15 @@ export const completedChallengesSelector = state =>
|
||||
userSelector(state).completedChallenges || [];
|
||||
export const partiallyCompletedChallengesSelector = state =>
|
||||
userSelector(state).partiallyCompletedChallenges || [];
|
||||
const completionCountSelector = state => state[MainApp].completionCount;
|
||||
export const currentChallengeIdSelector = state =>
|
||||
state[MainApp].currentChallengeId;
|
||||
|
||||
export const completionCountSelector = state => state[MainApp].completionCount;
|
||||
export const showMultipleProgressModalsSelector = state =>
|
||||
state[MainApp].showMultipleProgressModals;
|
||||
export const completionCountWhenShownProgressModalSelector = state =>
|
||||
state[MainApp].completionCountWhenShownProgressModal;
|
||||
export const progressDonationModalShownSelector = state =>
|
||||
state[MainApp].progressDonationModalShown;
|
||||
export const isDonatingSelector = state => userSelector(state).isDonating;
|
||||
export const isOnlineSelector = state => state[MainApp].isOnline;
|
||||
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
|
||||
@@ -29,11 +34,14 @@ export const showCertSelector = state => state[MainApp].showCert;
|
||||
export const showCertFetchStateSelector = state =>
|
||||
state[MainApp].showCertFetchState;
|
||||
export const shouldRequestDonationSelector = state => {
|
||||
const completedChallenges = completedChallengesSelector(state);
|
||||
const completedChallengesLength = completedChallengesSelector(state).length;
|
||||
const completionCount = completionCountSelector(state);
|
||||
const canRequestProgressDonation = state[MainApp].canRequestProgressDonation;
|
||||
const lastCompletionCount =
|
||||
completionCountWhenShownProgressModalSelector(state);
|
||||
const progressDonationModalShown = progressDonationModalShownSelector(state);
|
||||
const isDonating = isDonatingSelector(state);
|
||||
const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);
|
||||
const showMultipleProgressModals = showMultipleProgressModalsSelector(state);
|
||||
|
||||
// don't request donation if already donating
|
||||
if (isDonating) return false;
|
||||
@@ -41,14 +49,24 @@ export const shouldRequestDonationSelector = state => {
|
||||
// a block has been completed
|
||||
if (recentlyClaimedBlock) return true;
|
||||
|
||||
/*
|
||||
When AB testing for showing multiple progress modals is active,
|
||||
show a donation modal every 30 challenges after the first 50
|
||||
*/
|
||||
if (
|
||||
showMultipleProgressModals &&
|
||||
progressDonationModalShown &&
|
||||
completedChallengesLength > 50 &&
|
||||
completionCount - lastCompletionCount >= 30
|
||||
)
|
||||
return true;
|
||||
|
||||
// a donation has already been requested
|
||||
if (!canRequestProgressDonation) return false;
|
||||
if (progressDonationModalShown) return false;
|
||||
|
||||
// donations only appear after the user has completed ten challenges (i.e.
|
||||
// not before the 11th challenge has mounted)
|
||||
if (completedChallenges.length < 10) {
|
||||
return false;
|
||||
}
|
||||
if (completedChallengesLength < 10) return false;
|
||||
|
||||
// this will mean we have completed 3 or more challenges this browser session
|
||||
// and enough challenges overall to not be new
|
||||
|
||||
@@ -14,7 +14,8 @@ export interface State {
|
||||
[MainApp]: {
|
||||
appUsername: string;
|
||||
recentlyClaimedBlock: null | string;
|
||||
canRequestProgressDonation: boolean;
|
||||
completionCountWhenShownProgressModal: number | null;
|
||||
showMultipleProgressModals: boolean;
|
||||
completionCount: number;
|
||||
currentChallengId: string;
|
||||
showCert: Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user