diff --git a/client/src/components/growth-book/growth-book-redux-connector.tsx b/client/src/components/growth-book/growth-book-redux-connector.tsx new file mode 100644 index 00000000000..55c4602854a --- /dev/null +++ b/client/src/components/growth-book/growth-book-redux-connector.tsx @@ -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; +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); diff --git a/client/src/components/growth-book/growth-book-wrapper.tsx b/client/src/components/growth-book/growth-book-wrapper.tsx index 766ee6add35..b8d4f9362fc 100644 --- a/client/src/components/growth-book/growth-book-wrapper.tsx +++ b/client/src/components/growth-book/growth-book-wrapper.tsx @@ -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 ( - {children} + + {children} + ); }; diff --git a/client/src/redux/action-types.js b/client/src/redux/action-types.js index 0ff1882f6ce..ee6a281ad1f 100644 --- a/client/src/redux/action-types.js +++ b/client/src/redux/action-types.js @@ -10,7 +10,8 @@ export const actionTypes = createTypes( 'setRenderStartTime', 'hideCodeAlly', 'preventBlockDonationRequests', - 'preventProgressDonationRequests', + 'setCompletionCountWhenShownProgressModal', + 'setShowMultipleProgressModals', 'openDonationModal', 'closeDonationModal', 'openSignoutModal', diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js index 3af1cf04ee1..3f5dc58d508 100644 --- a/client/src/redux/actions.js +++ b/client/src/redux/actions.js @@ -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 diff --git a/client/src/redux/donation-saga.js b/client/src/redux/donation-saga.js index aaca6691cee..525165fc733 100644 --- a/client/src/redux/donation-saga.js +++ b/client/src/redux/donation-saga.js @@ -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()); } } } diff --git a/client/src/redux/index.js b/client/src/redux/index.js index b8d923a9c7a..ced9f0570f7 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -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, diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js index e0f05e7a80f..a7888e5aa80 100644 --- a/client/src/redux/selectors.js +++ b/client/src/redux/selectors.js @@ -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 diff --git a/client/src/redux/types.ts b/client/src/redux/types.ts index 4fbac24f854..c1f0cbf7fa5 100644 --- a/client/src/redux/types.ts +++ b/client/src/redux/types.ts @@ -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;