Merge branch 'onboarding-fix' into 'develop'
Start fixing onboarding See merge request soapbox-pub/soapbox-fe!1298
This commit is contained in:
commit
acd9311968
10 changed files with 204 additions and 39 deletions
|
@ -1,24 +1,101 @@
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { mockStore, mockWindowProperty } from 'soapbox/jest/test-helpers';
|
||||||
import { createTestStore, rootState } from 'soapbox/jest/test-helpers';
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
|
||||||
import { ONBOARDING_VERSION, endOnboarding } from '../onboarding';
|
import { checkOnboardingStatus, startOnboarding, endOnboarding } from '../onboarding';
|
||||||
|
|
||||||
describe('endOnboarding()', () => {
|
describe('checkOnboarding()', () => {
|
||||||
it('updates the onboardingVersion setting', async() => {
|
let mockGetItem: any;
|
||||||
const store = createTestStore(rootState);
|
|
||||||
|
|
||||||
// Sanity check:
|
mockWindowProperty('localStorage', {
|
||||||
// `onboardingVersion` should be `0` by default
|
getItem: (key: string) => mockGetItem(key),
|
||||||
const initialVersion = getSettings(store.getState()).get('onboardingVersion');
|
});
|
||||||
expect(initialVersion).toBe(0);
|
|
||||||
|
|
||||||
await store.dispatch(endOnboarding());
|
beforeEach(() => {
|
||||||
|
mockGetItem = jest.fn().mockReturnValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
// After dispatching, `onboardingVersion` is updated
|
it('does nothing if localStorage item is not set', async() => {
|
||||||
const updatedVersion = getSettings(store.getState()).get('onboardingVersion');
|
mockGetItem = jest.fn().mockReturnValue(null);
|
||||||
expect(updatedVersion).toBe(ONBOARDING_VERSION);
|
|
||||||
|
|
||||||
// Sanity check: `updatedVersion` is greater than `initialVersion`
|
const state = rootReducer(undefined, { onboarding: { needsOnboarding: false } });
|
||||||
expect(updatedVersion > initialVersion).toBe(true);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { createAccount } from 'soapbox/actions/accounts';
|
||||||
import { createApp } from 'soapbox/actions/apps';
|
import { createApp } from 'soapbox/actions/apps';
|
||||||
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
|
import { fetchMeSuccess, fetchMeFail } from 'soapbox/actions/me';
|
||||||
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
import { obtainOAuthToken, revokeOAuthToken } from 'soapbox/actions/oauth';
|
||||||
|
import { startOnboarding } from 'soapbox/actions/onboarding';
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
import { custom } from 'soapbox/custom';
|
import { custom } from 'soapbox/custom';
|
||||||
import KVStore from 'soapbox/storage/kv_store';
|
import KVStore from 'soapbox/storage/kv_store';
|
||||||
|
@ -292,7 +293,10 @@ export function register(params) {
|
||||||
|
|
||||||
return dispatch(createAppAndToken())
|
return dispatch(createAppAndToken())
|
||||||
.then(() => dispatch(createAccount(params)))
|
.then(() => dispatch(createAccount(params)))
|
||||||
.then(({ token }) => dispatch(authLoggedIn(token)));
|
.then(({ token }) => {
|
||||||
|
dispatch(startOnboarding());
|
||||||
|
return dispatch(authLoggedIn(token));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 */
|
const ONBOARDING_LOCAL_STORAGE_KEY = 'soapbox:onboarding';
|
||||||
export const ONBOARDING_VERSION = 1;
|
|
||||||
|
|
||||||
/** Finish onboarding and store the setting */
|
type OnboardingStartAction = {
|
||||||
const endOnboarding = () => (dispatch: React.Dispatch<any>) => {
|
type: typeof ONBOARDING_START
|
||||||
dispatch(changeSettingImmediate(['onboardingVersion'], ONBOARDING_VERSION));
|
}
|
||||||
|
|
||||||
|
type OnboardingEndAction = {
|
||||||
|
type: typeof ONBOARDING_END
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingActions = OnboardingStartAction | OnboardingEndAction
|
||||||
|
|
||||||
|
const checkOnboardingStatus = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
const needsOnboarding = localStorage.getItem(ONBOARDING_LOCAL_STORAGE_KEY) === '1';
|
||||||
|
|
||||||
|
if (needsOnboarding) {
|
||||||
|
dispatch({ type: ONBOARDING_START });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
localStorage.setItem(ONBOARDING_LOCAL_STORAGE_KEY, '1');
|
||||||
|
dispatch({ type: ONBOARDING_START });
|
||||||
|
};
|
||||||
|
|
||||||
|
const endOnboarding = () => (dispatch: React.Dispatch<OnboardingActions>) => {
|
||||||
|
localStorage.removeItem(ONBOARDING_LOCAL_STORAGE_KEY);
|
||||||
|
dispatch({ type: ONBOARDING_END });
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
ONBOARDING_END,
|
||||||
|
ONBOARDING_START,
|
||||||
|
checkOnboardingStatus,
|
||||||
endOnboarding,
|
endOnboarding,
|
||||||
|
startOnboarding,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const defaultSettings = ImmutableMap({
|
export const defaultSettings = ImmutableMap({
|
||||||
onboardingVersion: 0,
|
onboarded: false,
|
||||||
skinTone: 1,
|
skinTone: 1,
|
||||||
reduceMotion: false,
|
reduceMotion: false,
|
||||||
underlineLinks: false,
|
underlineLinks: false,
|
||||||
|
|
|
@ -25,7 +25,7 @@ import MESSAGES from 'soapbox/locales/messages';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||||
|
|
||||||
import { ONBOARDING_VERSION } from '../actions/onboarding';
|
import { checkOnboardingStatus } from '../actions/onboarding';
|
||||||
import { preload } from '../actions/preload';
|
import { preload } from '../actions/preload';
|
||||||
import ErrorBoundary from '../components/error_boundary';
|
import ErrorBoundary from '../components/error_boundary';
|
||||||
import UI from '../features/ui';
|
import UI from '../features/ui';
|
||||||
|
@ -40,6 +40,9 @@ createGlobals(store);
|
||||||
// Preload happens synchronously
|
// Preload happens synchronously
|
||||||
store.dispatch(preload() as any);
|
store.dispatch(preload() as any);
|
||||||
|
|
||||||
|
// This happens synchronously
|
||||||
|
store.dispatch(checkOnboardingStatus() as any);
|
||||||
|
|
||||||
/** Load initial data from the backend */
|
/** Load initial data from the backend */
|
||||||
const loadInitial = () => {
|
const loadInitial = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -76,7 +79,7 @@ const SoapboxMount = () => {
|
||||||
|
|
||||||
const locale = validLocale(settings.get('locale')) ? settings.get('locale') : 'en';
|
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 singleUserMode = soapboxConfig.singleUserMode && soapboxConfig.singleUserModeProfile;
|
||||||
|
|
||||||
const [messages, setMessages] = useState<Record<string, string>>({});
|
const [messages, setMessages] = useState<Record<string, string>>({});
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
|
||||||
import { Button } from 'soapbox/components/ui';
|
import { Button } from 'soapbox/components/ui';
|
||||||
import { Modal } from 'soapbox/components/ui';
|
import { Modal } from 'soapbox/components/ui';
|
||||||
|
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
download: { id: 'landing_page_modal.download', defaultMessage: 'Download' },
|
download: { id: 'landing_page_modal.download', defaultMessage: 'Download' },
|
||||||
|
@ -15,12 +13,19 @@ const messages = defineMessages({
|
||||||
register: { id: 'header.register.label', defaultMessage: 'Register' },
|
register: { id: 'header.register.label', defaultMessage: 'Register' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const LandingPageModal = ({ onClose }) => {
|
interface ILandingPageModal {
|
||||||
|
onClose: (type: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const logo = useSelector((state) => getSoapboxConfig(state).get('logo'));
|
const { logo } = useSoapboxConfig();
|
||||||
const instance = useSelector((state) => state.get('instance'));
|
const instance = useAppSelector((state) => state.instance);
|
||||||
const isOpen = instance.get('registrations', false) === true;
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const isOpen = instance.get('registrations', false) === true;
|
||||||
|
const pepeOpen = useAppSelector(state => state.verification.getIn(['instance', 'registrations'], false) === true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -38,8 +43,8 @@ const LandingPageModal = ({ onClose }) => {
|
||||||
{intl.formatMessage(messages.login)}
|
{intl.formatMessage(messages.login)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isOpen && (
|
{(isOpen || features.pepe && pepeOpen) && (
|
||||||
<Button to='/verify' theme='primary' block>
|
<Button to={features.pepe ? '/verify' : '/signup'} theme='primary' block>
|
||||||
{intl.formatMessage(messages.register)}
|
{intl.formatMessage(messages.register)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -49,8 +54,4 @@ const LandingPageModal = ({ onClose }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
LandingPageModal.propTypes = {
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LandingPageModal;
|
export default LandingPageModal;
|
|
@ -6,6 +6,7 @@ import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
|
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
|
||||||
import { fetchInstance } from 'soapbox/actions/instance';
|
import { fetchInstance } from 'soapbox/actions/instance';
|
||||||
|
import { startOnboarding } from 'soapbox/actions/onboarding';
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
import { createAccount } from 'soapbox/actions/verification';
|
import { createAccount } from 'soapbox/actions/verification';
|
||||||
import { removeStoredVerification } from 'soapbox/actions/verification';
|
import { removeStoredVerification } from 'soapbox/actions/verification';
|
||||||
|
@ -40,6 +41,7 @@ const Registration = () => {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setShouldRedirect(true);
|
setShouldRedirect(true);
|
||||||
removeStoredVerification();
|
removeStoredVerification();
|
||||||
|
dispatch(startOnboarding());
|
||||||
dispatch(
|
dispatch(
|
||||||
snackbar.success(
|
snackbar.success(
|
||||||
intl.formatMessage({
|
intl.formatMessage({
|
||||||
|
|
27
app/soapbox/reducers/__tests__/onboarding.test.ts
Normal file
27
app/soapbox/reducers/__tests__/onboarding.test.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -38,6 +38,7 @@ import meta from './meta';
|
||||||
import modals from './modals';
|
import modals from './modals';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
|
import onboarding from './onboarding';
|
||||||
import patron from './patron';
|
import patron from './patron';
|
||||||
import pending_statuses from './pending_statuses';
|
import pending_statuses from './pending_statuses';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
|
@ -117,6 +118,7 @@ const reducers = {
|
||||||
accounts_meta,
|
accounts_meta,
|
||||||
trending_statuses,
|
trending_statuses,
|
||||||
verification,
|
verification,
|
||||||
|
onboarding,
|
||||||
rules,
|
rules,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
22
app/soapbox/reducers/onboarding.ts
Normal file
22
app/soapbox/reducers/onboarding.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue