+
{t('No')}
diff --git a/src/components/Setup/Setup.graphql b/src/components/Setup/Setup.graphql
new file mode 100644
index 000000000..451e36964
--- /dev/null
+++ b/src/components/Setup/Setup.graphql
@@ -0,0 +1,12 @@
+query SetupStage {
+ user {
+ id
+ defaultAccountList
+ setup
+ }
+ userOptions {
+ id
+ key
+ value
+ }
+}
diff --git a/src/components/Setup/SetupProvider.test.tsx b/src/components/Setup/SetupProvider.test.tsx
new file mode 100644
index 000000000..18fd0d69e
--- /dev/null
+++ b/src/components/Setup/SetupProvider.test.tsx
@@ -0,0 +1,157 @@
+import { render, waitFor } from '@testing-library/react';
+import TestRouter from '__tests__/util/TestRouter';
+import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
+import { UserSetupStageEnum } from 'src/graphql/types.generated';
+import { SetupStageQuery } from './Setup.generated';
+import { SetupProvider, useSetupContext } from './SetupProvider';
+
+const push = jest.fn();
+
+interface TestComponentProps {
+ setup: UserSetupStageEnum | null;
+ setupPosition?: string | null;
+ pathname?: string;
+}
+
+const ContextTestingComponent = () => {
+ const { settingUp } = useSetupContext();
+
+ return (
+
+ {typeof settingUp === 'undefined' ? 'undefined' : settingUp.toString()}
+
+ );
+};
+
+const TestComponent: React.FC = ({
+ setup,
+ setupPosition = null,
+ pathname = '/',
+}) => (
+
+
+ mocks={{
+ SetupStage: {
+ user: {
+ setup,
+ },
+ userOptions: [
+ {
+ key: 'setup_position',
+ value: setupPosition,
+ },
+ ],
+ },
+ }}
+ >
+
+
+ Page content
+
+
+
+);
+
+describe('SetupProvider', () => {
+ beforeEach(() => {
+ process.env.DISABLE_SETUP_TOUR = undefined;
+ });
+
+ it('renders child content', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Page content')).toBeInTheDocument();
+ });
+
+ it('redirects if the user needs to connect to create an account list', async () => {
+ render();
+
+ await waitFor(() => expect(push).toHaveBeenCalledWith('/setup/connect'));
+ });
+
+ it('redirects if the user needs to connect to an organization', async () => {
+ render();
+
+ await waitFor(() => expect(push).toHaveBeenCalledWith('/setup/connect'));
+ });
+
+ it('redirects if the user needs to choose a default account', async () => {
+ render();
+
+ await waitFor(() => expect(push).toHaveBeenCalledWith('/setup/account'));
+ });
+
+ it('does not redirect if the user is on the setup start page', async () => {
+ render(
+ ,
+ );
+
+ await waitFor(() => expect(push).not.toHaveBeenCalled());
+ });
+
+ it('does not redirect if the user does not need to set up their account', async () => {
+ render();
+
+ await waitFor(() => expect(push).not.toHaveBeenCalled());
+ });
+
+ describe('settingUp context', () => {
+ it('is undefined while data is loading', () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId('setting-up')).toHaveTextContent('undefined');
+ });
+
+ it('is true when setup is set', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('setting-up')).toHaveTextContent('true'),
+ );
+ });
+
+ it('is true when setup_position is set', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('setting-up')).toHaveTextContent('true'),
+ );
+ });
+
+ it('is false when setup_position is not set', async () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('setting-up')).toHaveTextContent('false'),
+ );
+ });
+
+ it('is false when DISABLE_SETUP_TOUR is true', async () => {
+ process.env.DISABLE_SETUP_TOUR = 'true';
+
+ const { getByTestId } = render(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(getByTestId('setting-up')).toHaveTextContent('false'),
+ );
+ });
+ });
+});
diff --git a/src/components/Setup/SetupProvider.tsx b/src/components/Setup/SetupProvider.tsx
new file mode 100644
index 000000000..b84974af9
--- /dev/null
+++ b/src/components/Setup/SetupProvider.tsx
@@ -0,0 +1,83 @@
+import { useRouter } from 'next/router';
+import React, {
+ ReactNode,
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+} from 'react';
+import { UserSetupStageEnum } from 'src/graphql/types.generated';
+import { useSetupStageQuery } from './Setup.generated';
+
+export interface SetupContext {
+ settingUp?: boolean;
+}
+
+const SetupContext = createContext(null);
+
+export const useSetupContext = (): SetupContext => {
+ const setupContext = useContext(SetupContext);
+ if (!setupContext) {
+ throw new Error(
+ 'SetupProvider not found! Make sure that you are calling useSetupContext inside a component wrapped by .',
+ );
+ }
+
+ return setupContext;
+};
+
+interface Props {
+ children: ReactNode;
+}
+
+// This context component ensures that users have gone through the setup process
+// and provides the setup state to the rest of the application
+export const SetupProvider: React.FC = ({ children }) => {
+ const { data } = useSetupStageQuery();
+ const { push, pathname } = useRouter();
+
+ useEffect(() => {
+ if (
+ !data ||
+ pathname === '/setup/start' ||
+ process.env.DISABLE_SETUP_TOUR === 'true'
+ ) {
+ return;
+ }
+
+ // If the user hasn't completed crucial setup steps, take them to the tour
+ // to finish setting up their account. If they are on the preferences stage
+ // or beyond and manually typed in a URL, let them stay on the page they
+ // were on.
+ if (
+ data.user.setup === UserSetupStageEnum.NoAccountLists ||
+ data.user.setup === UserSetupStageEnum.NoOrganizationAccount
+ ) {
+ push('/setup/connect');
+ } else if (data.user.setup === UserSetupStageEnum.NoDefaultAccountList) {
+ push('/setup/account');
+ }
+ }, [data]);
+
+ const settingUp = useMemo(() => {
+ if (!data) {
+ return undefined;
+ }
+
+ if (process.env.DISABLE_SETUP_TOUR === 'true') {
+ return false;
+ }
+
+ return (
+ data.userOptions.some(
+ (option) => option.key === 'setup_position' && option.value !== '',
+ ) || data.user.setup !== null
+ );
+ }, [data]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Setup/useNextSetupPage.test.tsx b/src/components/Setup/useNextSetupPage.test.tsx
new file mode 100644
index 000000000..a9d27c87b
--- /dev/null
+++ b/src/components/Setup/useNextSetupPage.test.tsx
@@ -0,0 +1,110 @@
+import { ReactElement } from 'react';
+import { waitFor } from '@testing-library/react';
+import { renderHook } from '@testing-library/react-hooks';
+import TestRouter from '__tests__/util/TestRouter';
+import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
+import { UserSetupStageEnum } from 'src/graphql/types.generated';
+import { SetupStageQuery } from './Setup.generated';
+import { useNextSetupPage } from './useNextSetupPage';
+
+const push = jest.fn();
+const router = {
+ push,
+};
+
+interface HookWrapperProps {
+ setup: UserSetupStageEnum | null;
+ children: ReactElement;
+}
+
+const mutationSpy = jest.fn();
+
+const HookWrapper: React.FC = ({ setup, children }) => (
+
+
+ mocks={{
+ SetupStage: {
+ user: {
+ defaultAccountList: 'account-list-1',
+ setup,
+ },
+ },
+ }}
+ onCall={mutationSpy}
+ >
+ {children}
+
+
+);
+
+type HookWrapper = React.FC<{ children: ReactElement }>;
+
+describe('useNextSetupPage', () => {
+ it('when the user has no organization accounts next should redirect to the connect page', async () => {
+ const Wrapper: HookWrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ const { result } = renderHook(() => useNextSetupPage(), {
+ wrapper: Wrapper,
+ });
+ result.current.next();
+
+ await waitFor(() => expect(push).toHaveBeenCalledWith('/setup/connect'));
+ });
+
+ it('when the user has no account lists next should redirect to the connect page', async () => {
+ const Wrapper: HookWrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ const { result } = renderHook(() => useNextSetupPage(), {
+ wrapper: Wrapper,
+ });
+ result.current.next();
+
+ await waitFor(() => expect(push).toHaveBeenCalledWith('/setup/connect'));
+ });
+
+ it('when the user has no default account list next should redirect to the account page', async () => {
+ const Wrapper: HookWrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ const { result } = renderHook(() => useNextSetupPage(), {
+ wrapper: Wrapper,
+ });
+ result.current.next();
+
+ await waitFor(() => expect(push).toHaveBeenCalledWith('/setup/account'));
+ });
+
+ it("when the user's account is set up next should set setup_position and redirect to the preferences page", async () => {
+ const Wrapper: HookWrapper = ({ children }) => (
+ {children}
+ );
+
+ const { result } = renderHook(() => useNextSetupPage(), {
+ wrapper: Wrapper,
+ });
+ result.current.next();
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation('UpdateUserOptions', {
+ key: 'setup_position',
+ value: 'preferences.personal',
+ }),
+ );
+ await waitFor(() =>
+ expect(push).toHaveBeenCalledWith(
+ '/accountLists/account-list-1/settings/preferences',
+ ),
+ );
+ });
+});
diff --git a/src/components/Setup/useNextSetupPage.ts b/src/components/Setup/useNextSetupPage.ts
new file mode 100644
index 000000000..9829197ab
--- /dev/null
+++ b/src/components/Setup/useNextSetupPage.ts
@@ -0,0 +1,49 @@
+import { useRouter } from 'next/router';
+import { useCallback } from 'react';
+import { UserSetupStageEnum } from 'src/graphql/types.generated';
+import { useUpdateUserOptionsMutation } from '../Contacts/ContactFlow/ContactFlowSetup/UpdateUserOptions.generated';
+import { useSetupStageLazyQuery } from './Setup.generated';
+
+interface UseNextSetupPageResult {
+ // Advance to the next setup page
+ next: () => Promise;
+}
+
+export const useNextSetupPage = (): UseNextSetupPageResult => {
+ const { push } = useRouter();
+ const [getSetupStage] = useSetupStageLazyQuery();
+ const [updateUserOptions] = useUpdateUserOptionsMutation();
+
+ const saveSetupPosition = (setupPosition: string) =>
+ updateUserOptions({
+ variables: {
+ key: 'setup_position',
+ value: setupPosition,
+ },
+ });
+
+ const next = useCallback(async () => {
+ const { data } = await getSetupStage();
+ switch (data?.user.setup) {
+ case UserSetupStageEnum.NoAccountLists:
+ case UserSetupStageEnum.NoOrganizationAccount:
+ push('/setup/connect');
+ return;
+
+ case UserSetupStageEnum.NoDefaultAccountList:
+ push('/setup/account');
+ return;
+
+ case null:
+ await saveSetupPosition('preferences.personal');
+ push(
+ `/accountLists/${data.user.defaultAccountList}/settings/preferences`,
+ );
+ return;
+ }
+ }, []);
+
+ return {
+ next,
+ };
+};