From 35a731ffd9fd4839e22f10978ab6e4dc845a5a06 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 2 May 2022 15:55:52 -0500 Subject: [PATCH 1/4] 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; + } +} From 23fb01d32d4c2e125093c4d73c019f35f2185ea3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 2 May 2022 16:24:19 -0500 Subject: [PATCH 2/4] Set onboarding on account creation --- app/soapbox/actions/auth.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 00f9c606d..79ff9d67f 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -13,6 +13,7 @@ import { createAccount } from 'soapbox/actions/accounts'; import { createApp } from 'soapbox/actions/apps'; import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me'; import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth'; +import { startOnboarding } from 'soapbox/actions/onboarding'; import snackbar from 'soapbox/actions/snackbar'; import { custom } from 'soapbox/custom'; import KVStore from 'soapbox/storage/kv_store'; @@ -292,7 +293,10 @@ export function register(params) { return dispatch(createAppAndToken()) .then(() => dispatch(createAccount(params))) - .then(({ token }) => dispatch(authLoggedIn(token))); + .then(({ token }) => { + dispatch(startOnboarding()); + return dispatch(authLoggedIn(token)); + }); }; } From 2028873d34a834007f54897cc27370682b73475b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 2 May 2022 16:24:45 -0500 Subject: [PATCH 3/4] LandingPageModal: fix routes --- .../ui/components/modals/landing-page-modal.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/soapbox/features/ui/components/modals/landing-page-modal.js b/app/soapbox/features/ui/components/modals/landing-page-modal.js index d9b40d7b9..9fa8784bb 100644 --- a/app/soapbox/features/ui/components/modals/landing-page-modal.js +++ b/app/soapbox/features/ui/components/modals/landing-page-modal.js @@ -2,11 +2,10 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useSelector } from 'react-redux'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { Button } from 'soapbox/components/ui'; import { Modal } from 'soapbox/components/ui'; +import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; const messages = defineMessages({ download: { id: 'landing_page_modal.download', defaultMessage: 'Download' }, @@ -18,9 +17,12 @@ const messages = defineMessages({ const LandingPageModal = ({ onClose }) => { const intl = useIntl(); - const logo = useSelector((state) => getSoapboxConfig(state).get('logo')); - const instance = useSelector((state) => state.get('instance')); - const isOpen = instance.get('registrations', false) === true; + const { logo } = useSoapboxConfig(); + const instance = useAppSelector((state) => state.instance); + const features = useFeatures(); + + const isOpen = instance.get('registrations', false) === true; + const pepeOpen = useAppSelector(state => state.verification.getIn(['instance', 'registrations'], false) === true); return ( { {intl.formatMessage(messages.login)} - {isOpen && ( - )} From e11575ff50ce5348750527a252c156276168875e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 2 May 2022 16:26:27 -0500 Subject: [PATCH 4/4] LandingPageModal: convert to tsx --- .../{landing-page-modal.js => landing-page-modal.tsx} | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) rename app/soapbox/features/ui/components/modals/{landing-page-modal.js => landing-page-modal.tsx} (92%) diff --git a/app/soapbox/features/ui/components/modals/landing-page-modal.js b/app/soapbox/features/ui/components/modals/landing-page-modal.tsx similarity index 92% rename from app/soapbox/features/ui/components/modals/landing-page-modal.js rename to app/soapbox/features/ui/components/modals/landing-page-modal.tsx index 9fa8784bb..c6e47b20c 100644 --- a/app/soapbox/features/ui/components/modals/landing-page-modal.js +++ b/app/soapbox/features/ui/components/modals/landing-page-modal.tsx @@ -1,5 +1,4 @@ import classNames from 'classnames'; -import PropTypes from 'prop-types'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -14,7 +13,11 @@ const messages = defineMessages({ register: { id: 'header.register.label', defaultMessage: 'Register' }, }); -const LandingPageModal = ({ onClose }) => { +interface ILandingPageModal { + onClose: (type: string) => void, +} + +const LandingPageModal: React.FC = ({ onClose }) => { const intl = useIntl(); const { logo } = useSoapboxConfig(); @@ -51,8 +54,4 @@ const LandingPageModal = ({ onClose }) => { ); }; -LandingPageModal.propTypes = { - onClose: PropTypes.func.isRequired, -}; - export default LandingPageModal;