Skip to content

Commit

Permalink
feat: move session related challenge data to sessionStorage (freeCode…
Browse files Browse the repository at this point in the history
…Camp#55918)

Co-authored-by: sembauke <[email protected]>
Co-authored-by: Oliver Eyton-Williams <[email protected]>
  • Loading branch information
3 people authored Sep 13, 2024
1 parent 6596d8f commit 0ee8097
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 44 deletions.
1 change: 0 additions & 1 deletion client/src/redux/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export const actionTypes = createTypes(
'allowBlockDonationRequests',
'setRenderStartTime',
'preventBlockDonationRequests',
'setCompletionCountWhenShownProgressModal',
'setShowMultipleProgressModals',
'openDonationModal',
'closeDonationModal',
Expand Down
4 changes: 1 addition & 3 deletions client/src/redux/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ export const openDonationModal = createAction(actionTypes.openDonationModal);
export const preventBlockDonationRequests = createAction(
actionTypes.preventBlockDonationRequests
);
export const setCompletionCountWhenShownProgressModal = createAction(
actionTypes.setCompletionCountWhenShownProgressModal
);

export const setShowMultipleProgressModals = createAction(
actionTypes.setShowMultipleProgressModals
);
Expand Down
13 changes: 7 additions & 6 deletions client/src/redux/donation-saga.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ import {
import { stringifyDonationEvents } from '../utils/analytics-strings';
import { stripe } from '../utils/stripe';
import { PaymentProvider } from '../../../shared/config/donation-settings';
import {
getSessionChallengeData,
saveCurrentCount
} from '../utils/session-storage';
import { actionTypes as appTypes } from './action-types';
import {
openDonationModal,
postChargeComplete,
postChargeProcessing,
postChargeError,
preventBlockDonationRequests,
setCompletionCountWhenShownProgressModal,
updateCardError,
updateCardRedirecting
} from './actions';
Expand All @@ -34,7 +37,6 @@ import {
recentlyClaimedBlockSelector,
shouldRequestDonationSelector,
isSignedInSelector,
completionCountSelector,
completedChallengesSelector
} from './selectors';

Expand All @@ -56,7 +58,7 @@ function* showDonateModalSaga() {
if (recentlyClaimedBlock) {
yield put(preventBlockDonationRequests());
} else {
yield put(setCompletionCountWhenShownProgressModal());
yield call(saveCurrentCount);
}
}
}
Expand Down Expand Up @@ -124,9 +126,8 @@ export function* postChargeSaga({
});
} else {
const completedChallenges = yield select(completedChallengesSelector);
const completedChallengesInSession = yield select(
completionCountSelector
);
const sessionChallengeData = yield call(getSessionChallengeData);
const completedChallengesInSession = sessionChallengeData.currentCount;
yield call(callGA, {
event: 'donation',
action: stringifyDonationEvents(paymentContext, paymentProvider),
Expand Down
8 changes: 5 additions & 3 deletions client/src/redux/donation-saga.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ const analyticsDataMock = {
const signedInStoreMock = {
app: {
appUsername: 'devuser',
completionCount: 2,
user: {
devuser: {
completedChallenges: [
Expand Down Expand Up @@ -83,7 +82,6 @@ const signedInStoreMock = {
const signedOutStoreMock = {
app: {
appUsername: '',
completionCount: 0,
user: {
'': {
completedChallenges: []
Expand All @@ -94,6 +92,9 @@ const signedOutStoreMock = {

describe('donation-saga', () => {
it('calls postChargeStrip for Stripe', () => {
// The number of completed challenges per session is stored in the session storage
sessionStorage.setItem('session-completed-challenges', '2');

return expectSaga(postChargeSaga, postChargeDataMock)
.withState(signedInStoreMock)
.put(postChargeProcessing())
Expand Down Expand Up @@ -149,6 +150,8 @@ describe('donation-saga', () => {
payload: { ...postChargeDataMock.payload, paymentProvider: 'paypal' }
};

sessionStorage.setItem('session-completed-challenges', '0');

const paypalAnalyticsDataMock = {
...analyticsDataMock,
action: 'Donate Page Paypal Payment Submission',
Expand All @@ -160,7 +163,6 @@ describe('donation-saga', () => {
const signedOutStoreMock = {
app: {
appUsername: '',
completionCount: 0,
user: {
'': {
completedChallenges: []
Expand Down
17 changes: 4 additions & 13 deletions client/src/redux/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import { createFetchUserSaga } from './fetch-user-saga';
import hardGoToEpic from './hard-go-to-epic';
import { createReportUserSaga } from './report-user-saga';
import { createSaveChallengeSaga } from './save-challenge-saga';
import { completionCountSelector, savedChallengesSelector } from './selectors';
import { savedChallengesSelector } from './selectors';
import { actionTypes as settingsTypes } from './settings/action-types';
import { createShowCertSaga } from './show-cert-saga';
import updateCompleteEpic from './update-complete-epic';
import { createUserTokenSaga } from './user-token-saga';
import { createMsUsernameSaga } from './ms-username-saga';
import { createSurveySaga } from './survey-saga';
import { createSessionCompletedChallengesSaga } from './session-completed-challenges';

const defaultFetchState = {
pending: true,
Expand Down Expand Up @@ -51,9 +52,6 @@ const initialState = {
appUsername: '',
showMultipleProgressModals: false,
recentlyClaimedBlock: null,
completionCountWhenShownProgressModal: 0,
progressDonationModalShown: false,
completionCount: 0,
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
examInProgress: false,
isProcessing: false,
Expand Down Expand Up @@ -97,7 +95,8 @@ export const sagas = [
...createUserTokenSaga(actionTypes),
...createSaveChallengeSaga(actionTypes),
...createMsUsernameSaga(actionTypes),
...createSurveySaga(actionTypes)
...createSurveySaga(actionTypes),
...createSessionCompletedChallengesSaga(actionTypes)
];

function spreadThePayloadOnUser(state, payload) {
Expand Down Expand Up @@ -274,13 +273,6 @@ export const reducer = handleActions(
...state,
recentlyClaimedBlock: null
}),
[actionTypes.setCompletionCountWhenShownProgressModal]: state => ({
...state,
progressDonationModalShown: true,
completionCountWhenShownProgressModal: completionCountSelector({
[MainApp]: state
})
}),
[actionTypes.setShowMultipleProgressModals]: (state, { payload }) => ({
...state,
showMultipleProgressModals: payload
Expand Down Expand Up @@ -346,7 +338,6 @@ export const reducer = handleActions(
}
: {
...state,
completionCount: state.completionCount + 1,
user: {
...state.user,
[appUsername]: {
Expand Down
27 changes: 11 additions & 16 deletions client/src/redux/selectors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Certification } from '../../../shared/config/certification-settings';
import { getSessionChallengeData } from '../utils/session-storage';
import { ns as MainApp } from './action-types';

export const savedChallengesSelector = state =>
Expand All @@ -13,10 +14,6 @@ export const currentChallengeIdSelector = state =>
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;
Expand All @@ -36,11 +33,7 @@ export const showCertSelector = state => state[MainApp].showCert;
export const showCertFetchStateSelector = state =>
state[MainApp].showCertFetchState;
export const shouldRequestDonationSelector = state => {
const completedChallengesLength = completedChallengesSelector(state).length;
const completionCount = completionCountSelector(state);
const lastCompletionCount =
completionCountWhenShownProgressModalSelector(state);
const progressDonationModalShown = progressDonationModalShownSelector(state);
const completedChallengeCount = completedChallengesSelector(state).length;
const isDonating = isDonatingSelector(state);
const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);

Expand All @@ -50,22 +43,24 @@ export const shouldRequestDonationSelector = state => {
// a block has been completed
if (recentlyClaimedBlock) return true;

const sessionChallengeData = getSessionChallengeData();
/*
Different intervals need to be tested for optimization.
*/
if (progressDonationModalShown && completionCount - lastCompletionCount >= 20)
return true;

// a donation has already been requested
if (progressDonationModalShown) return false;
// the assumption is that we save the count when we request donations
if (sessionChallengeData.isSaved) {
// only request if sufficient challenges have been completed since last
// request
return sessionChallengeData.countSinceSave >= 20;
}

// donations only appear after the user has completed ten challenges (i.e.
// not before the 11th challenge has mounted)
if (completedChallengesLength < 10) return false;
if (completedChallengeCount < 10) return false;

// this will mean we have completed 3 or more challenges this browser session
// and enough challenges overall to not be new
return completionCount >= 3;
return sessionChallengeData.currentCount >= 3;
};

export const userTokenSelector = state => {
Expand Down
11 changes: 11 additions & 0 deletions client/src/redux/session-completed-challenges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { takeEvery, call } from 'redux-saga/effects';

import { incrementCurrentCount } from '../utils/session-storage';

function* SessionCompletedChallengesSaga() {
yield call(incrementCurrentCount);
}

export function createSessionCompletedChallengesSaga(types) {
return [takeEvery(types.submitComplete, SessionCompletedChallengesSaga)];
}
2 changes: 0 additions & 2 deletions client/src/redux/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ export interface State {
[MainApp]: {
appUsername: string;
recentlyClaimedBlock: null | string;
completionCountWhenShownProgressModal: number | null;
showMultipleProgressModals: boolean;
completionCount: number;
currentChallengId: string;
showCert: Record<string, unknown>;
showCertFetchState: DefaultFetchState;
Expand Down
134 changes: 134 additions & 0 deletions client/src/utils/session-storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/unbound-method */
import {
CURRENT_COUNT_KEY,
SAVED_COUNT_KEY,
getSessionChallengeData,
incrementCurrentCount,
saveCurrentCount
} from './session-storage';

describe('Session Storage', () => {
let sessionStorageMock: Storage;

beforeEach(() => {
sessionStorageMock = (() => {
let store: { [key: string]: string } = {};

const mockStorage: Storage = {
length: 0,

clear: jest.fn(() => {
store = {};
}),

getItem: jest.fn((key: string) => {
return store[key] || null;
}),

key: jest.fn((index: number) => {
const keys = Object.keys(store);
return keys[index] || null;
}),

removeItem: jest.fn((key: string) => {
delete store[key];
}),

setItem: jest.fn((key: string, value: string) => {
store[key] = value;
})
};

Object.defineProperty(mockStorage, 'length', {
get: () => Object.keys(store).length
});

return mockStorage;
})();

Object.defineProperty(window, 'sessionStorage', {
value: sessionStorageMock,
configurable: true
});
});

afterEach(() => {
sessionStorage.clear();
jest.clearAllMocks();
});

describe('getSessionChallengeData', () => {
describe('countSinceSave', () => {
it('is not included if nothing has been saved', () => {
expect(getSessionChallengeData()).not.toHaveProperty('countSinceSave');
});

it('is included if the count has been saved', () => {
sessionStorage.setItem(SAVED_COUNT_KEY, '7');
sessionStorage.setItem(CURRENT_COUNT_KEY, '10');
expect(getSessionChallengeData()).toMatchObject({
countSinceSave: 3
});
});
});

describe('currentCount', () => {
it('defaults to 0 if no challenges have been completed', () => {
expect(getSessionChallengeData()).toMatchObject({
currentCount: 0
});
});

it('returns the stored number if it exists', () => {
sessionStorage.setItem(CURRENT_COUNT_KEY, '5');
expect(getSessionChallengeData()).toMatchObject({
currentCount: 5
});
});
});

describe('isSaved', () => {
it('is false if we haved saved the count', () => {
expect(getSessionChallengeData()).toMatchObject({
isSaved: false
});
});

it('is true if we have saved something', () => {
sessionStorage.setItem(SAVED_COUNT_KEY, '7');
expect(getSessionChallengeData()).toMatchObject({
isSaved: true
});
});
});
});

describe('incrementCurrentCount', () => {
test('increments the completed challenge count', () => {
sessionStorage.setItem(CURRENT_COUNT_KEY, '5');
incrementCurrentCount();
expect(sessionStorage.setItem).toHaveBeenCalledWith(
CURRENT_COUNT_KEY,
'6'
);
expect(getSessionChallengeData()).toMatchObject({ currentCount: 6 });
});

test('sets the count to 1 if no previous value exists', () => {
incrementCurrentCount();
expect(sessionStorage.setItem).toHaveBeenCalledWith(
CURRENT_COUNT_KEY,
'1'
);
expect(getSessionChallengeData()).toMatchObject({ currentCount: 1 });
});
});

describe('saveCurrentCount', () => {
test('sets correct value in sessionStorage', () => {
sessionStorage.setItem(CURRENT_COUNT_KEY, '5');
saveCurrentCount();
expect(sessionStorage.setItem).toHaveBeenCalledWith(SAVED_COUNT_KEY, '5');
});
});
});
Loading

0 comments on commit 0ee8097

Please sign in to comment.