From 35a731ffd9fd4839e22f10978ab6e4dc845a5a06 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 2 May 2022 15:55:52 -0500 Subject: [PATCH] Restore localStorage onboarding code --- .../actions/__tests__/onboarding.test.ts | 109 +++++++++++++++--- app/soapbox/actions/onboarding.ts | 39 ++++++- app/soapbox/actions/settings.js | 2 +- app/soapbox/containers/soapbox.tsx | 7 +- .../features/verification/registration.tsx | 2 + .../reducers/__tests__/onboarding.test.ts | 27 +++++ app/soapbox/reducers/index.ts | 2 + app/soapbox/reducers/onboarding.ts | 22 ++++ 8 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 app/soapbox/reducers/__tests__/onboarding.test.ts create mode 100644 app/soapbox/reducers/onboarding.ts diff --git a/app/soapbox/actions/__tests__/onboarding.test.ts b/app/soapbox/actions/__tests__/onboarding.test.ts index 08ca76284..cdd268ed5 100644 --- a/app/soapbox/actions/__tests__/onboarding.test.ts +++ b/app/soapbox/actions/__tests__/onboarding.test.ts @@ -1,24 +1,101 @@ -import { getSettings } from 'soapbox/actions/settings'; -import { createTestStore, rootState } from 'soapbox/jest/test-helpers'; +import { mockStore, mockWindowProperty } from 'soapbox/jest/test-helpers'; +import rootReducer from 'soapbox/reducers'; -import { ONBOARDING_VERSION, endOnboarding } from '../onboarding'; +import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding'; -describe('endOnboarding()', () => { - it('updates the onboardingVersion setting', async() => { - const store = createTestStore(rootState); +describe('checkOnboarding()', () => { + let mockGetItem: any; - // Sanity check: - // `onboardingVersion` should be `0` by default - const initialVersion = getSettings(store.getState()).get('onboardingVersion'); - expect(initialVersion).toBe(0); + mockWindowProperty('localStorage', { + getItem: (key: string) => mockGetItem(key), + }); - await store.dispatch(endOnboarding()); + beforeEach(() => { + mockGetItem = jest.fn().mockReturnValue(null); + }); - // After dispatching, `onboardingVersion` is updated - const updatedVersion = getSettings(store.getState()).get('onboardingVersion'); - expect(updatedVersion).toBe(ONBOARDING_VERSION); + it('does nothing if localStorage item is not set', async() => { + mockGetItem = jest.fn().mockReturnValue(null); - // Sanity check: `updatedVersion` is greater than `initialVersion` - expect(updatedVersion > initialVersion).toBe(true); + const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); + const store = mockStore(state); + + await store.dispatch(checkOnboardingStatus()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + expect(mockGetItem.mock.calls.length).toBe(1); + }); + + it('does nothing if localStorage item is invalid', async() => { + mockGetItem = jest.fn().mockReturnValue('invalid'); + + const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); + const store = mockStore(state); + + await store.dispatch(checkOnboardingStatus()); + const actions = store.getActions(); + + expect(actions).toEqual([]); + expect(mockGetItem.mock.calls.length).toBe(1); + }); + + it('dispatches the correct action', async() => { + mockGetItem = jest.fn().mockReturnValue('1'); + + const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); + const store = mockStore(state); + + await store.dispatch(checkOnboardingStatus()); + const actions = store.getActions(); + + expect(actions).toEqual([{ type: 'ONBOARDING_START' }]); + expect(mockGetItem.mock.calls.length).toBe(1); + }); +}); + +describe('startOnboarding()', () => { + let mockSetItem: any; + + mockWindowProperty('localStorage', { + setItem: (key: string, value: string) => mockSetItem(key, value), + }); + + beforeEach(() => { + mockSetItem = jest.fn(); + }); + + it('dispatches the correct action', async() => { + const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); + const store = mockStore(state); + + await store.dispatch(startOnboarding()); + const actions = store.getActions(); + + expect(actions).toEqual([{ type: 'ONBOARDING_START' }]); + expect(mockSetItem.mock.calls.length).toBe(1); + }); +}); + +describe('endOnboarding()', () => { + let mockRemoveItem: any; + + mockWindowProperty('localStorage', { + removeItem: (key: string) => mockRemoveItem(key), + }); + + beforeEach(() => { + mockRemoveItem = jest.fn(); + }); + + it('dispatches the correct action', async() => { + const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } }); + const store = mockStore(state); + + await store.dispatch(endOnboarding()); + const actions = store.getActions(); + + expect(actions).toEqual([{ type: 'ONBOARDING_END' }]); + expect(mockRemoveItem.mock.calls.length).toBe(1); }); }); diff --git a/app/soapbox/actions/onboarding.ts b/app/soapbox/actions/onboarding.ts index 13bcf0f73..ff12bd074 100644 --- a/app/soapbox/actions/onboarding.ts +++ b/app/soapbox/actions/onboarding.ts @@ -1,13 +1,40 @@ -import { changeSettingImmediate } from 'soapbox/actions/settings'; +const ONBOARDING_START = 'ONBOARDING_START'; +const ONBOARDING_END = 'ONBOARDING_END'; -/** Repeat the onboading process when we bump the version */ -export const ONBOARDING_VERSION = 1; +const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding'; -/** Finish onboarding and store the setting */ -const endOnboarding = () => (dispatch: React.Dispatch) => { - dispatch(changeSettingImmediate(['onboardingVersion'], ONBOARDING_VERSION)); +type OnboardingStartAction = { + type: typeof ONBOARDING_START +} + +type OnboardingEndAction = { + type: typeof ONBOARDING_END +} + +export type OnboardingActions = OnboardingStartAction | OnboardingEndAction + +const checkOnboardingStatus = () => (dispatch: React.Dispatch) => { + const needsOnboarding = localStorage.getItem(ONBOARDING_LOCAL_STORAGE_KEY) === '1'; + + if (needsOnboarding) { + dispatch({ type: ONBOARDING_START }); + } +}; + +const startOnboarding = () => (dispatch: React.Dispatch) => { + localStorage.setItem(ONBOARDING_LOCAL_STORAGE_KEY, '1'); + dispatch({ type: ONBOARDING_START }); +}; + +const endOnboarding = () => (dispatch: React.Dispatch) => { + localStorage.removeItem(ONBOARDING_LOCAL_STORAGE_KEY); + dispatch({ type: ONBOARDING_END }); }; export { + ONBOARDING_END, + ONBOARDING_START, + checkOnboardingStatus, endOnboarding, + startOnboarding, }; diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 8a76a2c6c..098cdfdb1 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -20,7 +20,7 @@ const messages = defineMessages({ }); export const defaultSettings = ImmutableMap({ - onboardingVersion: 0, + onboarded: false, skinTone: 1, reduceMotion: false, underlineLinks: false, diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 0b27ee33e..232e57817 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -25,7 +25,7 @@ import MESSAGES from 'soapbox/locales/messages'; import { getFeatures } from 'soapbox/utils/features'; import { generateThemeCss } from 'soapbox/utils/theme'; -import { ONBOARDING_VERSION } from '../actions/onboarding'; +import { checkOnboardingStatus } from '../actions/onboarding'; import { preload } from '../actions/preload'; import ErrorBoundary from '../components/error_boundary'; import UI from '../features/ui'; @@ -40,6 +40,9 @@ createGlobals(store); // Preload happens synchronously store.dispatch(preload() as any); +// This happens synchronously +store.dispatch(checkOnboardingStatus() as any); + /** Load initial data from the backend */ const loadInitial = () => { // @ts-ignore @@ -76,7 +79,7 @@ const SoapboxMount = () => { const locale = validLocale(settings.get('locale')) ? settings.get('locale') : 'en'; - const needsOnboarding = settings.get('onboardingVersion') < ONBOARDING_VERSION; + const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const singleUserMode = soapboxConfig.singleUserMode && soapboxConfig.singleUserModeProfile; const [messages, setMessages] = useState>({}); diff --git a/app/soapbox/features/verification/registration.tsx b/app/soapbox/features/verification/registration.tsx index 9d87ae410..9fd4808dc 100644 --- a/app/soapbox/features/verification/registration.tsx +++ b/app/soapbox/features/verification/registration.tsx @@ -6,6 +6,7 @@ import { Redirect } from 'react-router-dom'; import { logIn, verifyCredentials } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; +import { startOnboarding } from 'soapbox/actions/onboarding'; import snackbar from 'soapbox/actions/snackbar'; import { createAccount } from 'soapbox/actions/verification'; import { removeStoredVerification } from 'soapbox/actions/verification'; @@ -40,6 +41,7 @@ const Registration = () => { .then(() => { setShouldRedirect(true); removeStoredVerification(); + dispatch(startOnboarding()); dispatch( snackbar.success( intl.formatMessage({ diff --git a/app/soapbox/reducers/__tests__/onboarding.test.ts b/app/soapbox/reducers/__tests__/onboarding.test.ts new file mode 100644 index 000000000..95ecdf755 --- /dev/null +++ b/app/soapbox/reducers/__tests__/onboarding.test.ts @@ -0,0 +1,27 @@ +import { ONBOARDING_START, ONBOARDING_END } from 'soapbox/actions/onboarding'; + +import reducer from '../onboarding'; + +describe('onboarding reducer', () => { + it('should return the initial state', () => { + expect(reducer(undefined, {})).toEqual({ + needsOnboarding: false, + }); + }); + + describe('ONBOARDING_START', () => { + it('sets "needsOnboarding" to "true"', () => { + const initialState = { needsOnboarding: false }; + const action = { type: ONBOARDING_START }; + expect(reducer(initialState, action).needsOnboarding).toEqual(true); + }); + }); + + describe('ONBOARDING_END', () => { + it('sets "needsOnboarding" to "false"', () => { + const initialState = { needsOnboarding: true }; + const action = { type: ONBOARDING_END }; + expect(reducer(initialState, action).needsOnboarding).toEqual(false); + }); + }); +}); diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index 7e723d27c..a8c225d31 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -38,6 +38,7 @@ import meta from './meta'; import modals from './modals'; import mutes from './mutes'; import notifications from './notifications'; +import onboarding from './onboarding'; import patron from './patron'; import pending_statuses from './pending_statuses'; import polls from './polls'; @@ -117,6 +118,7 @@ const reducers = { accounts_meta, trending_statuses, verification, + onboarding, rules, }; diff --git a/app/soapbox/reducers/onboarding.ts b/app/soapbox/reducers/onboarding.ts new file mode 100644 index 000000000..844d6b353 --- /dev/null +++ b/app/soapbox/reducers/onboarding.ts @@ -0,0 +1,22 @@ +import { ONBOARDING_START, ONBOARDING_END } from 'soapbox/actions/onboarding'; + +import type { OnboardingActions } from 'soapbox/actions/onboarding'; + +type OnboardingState = { + needsOnboarding: boolean, +} + +const initialState: OnboardingState = { + needsOnboarding: false, +}; + +export default function onboarding(state: OnboardingState = initialState, action: OnboardingActions): OnboardingState { + switch(action.type) { + case ONBOARDING_START: + return { ...state, needsOnboarding: true }; + case ONBOARDING_END: + return { ...state, needsOnboarding: false }; + default: + return state; + } +}