0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(ui): refactor ui hooks and provider structure (#3647)

* refactor(ui): refactor ui hooks and provider structure

refactor ui hooks and provider structure

* chore(ui): provide dependency precisely

provide dependency precisely
This commit is contained in:
simeng-li 2023-04-03 10:24:03 +08:00 committed by GitHub
parent e6394b07b3
commit fc08fb5575
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 495 additions and 441 deletions

View file

@ -1,13 +1,10 @@
import { SignInMode } from '@logto/schemas';
import { useEffect, useRef } from 'react';
import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
import { Route, Routes, BrowserRouter } from 'react-router-dom';
import AppLayout from './Layout/AppLayout';
import AppBoundary from './Providers/AppBoundary';
import LoadingLayerProvider from './Providers/LoadingLayerProvider';
import usePageContext from './hooks/use-page-context';
import usePreview from './hooks/use-preview';
import initI18n from './i18n/init';
import PageContextProvider from './Providers/PageContextProvider';
import SettingsProvider from './Providers/SettingsProvider';
import Callback from './pages/Callback';
import Consent from './pages/Consent';
import Continue from './pages/Continue';
@ -24,117 +21,68 @@ import SocialSignIn from './pages/SocialSignInCallback';
import Springboard from './pages/Springboard';
import VerificationCode from './pages/VerificationCode';
import { handleSearchParametersData } from './utils/search-parameters';
import { getSignInExperienceSettings, setFavIcon } from './utils/sign-in-experience';
import './scss/normalized.scss';
handleSearchParametersData();
const App = () => {
const { context, Provider } = usePageContext();
const { experienceSettings, setLoading, setExperienceSettings } = context;
const customCssRef = useRef(document.createElement('style'));
const [isPreview, previewConfig] = usePreview(context);
useEffect(() => {
document.head.append(customCssRef.current);
}, []);
useEffect(() => {
if (isPreview) {
// eslint-disable-next-line @silverhand/fp/no-mutation
customCssRef.current.textContent = previewConfig?.signInExperience.customCss ?? null;
return;
}
(async () => {
const settings = await getSignInExperienceSettings();
const {
customCss,
branding: { favicon },
} = settings;
// eslint-disable-next-line @silverhand/fp/no-mutation
customCssRef.current.textContent = customCss;
setFavIcon(favicon);
// Note: i18n must be initialized ahead of page render
await initI18n();
// Init the page settings and render
setExperienceSettings(settings);
})();
}, [isPreview, previewConfig, setExperienceSettings, setLoading]);
if (!experienceSettings) {
return null;
}
const isRegisterOnly = experienceSettings.signInMode === SignInMode.Register;
const isSignInOnly = experienceSettings.signInMode === SignInMode.SignIn;
return (
<BrowserRouter>
<Provider value={context}>
<AppBoundary>
<Routes>
<Route path="sign-in/consent" element={<Consent />} />
<Route element={<AppLayout />}>
<Route
path="unknown-session"
element={<ErrorPage message="error.invalid_session" />}
/>
<Route path="springboard" element={<Springboard />} />
<PageContextProvider>
<SettingsProvider>
<AppBoundary>
<Routes>
<Route path="sign-in/consent" element={<Consent />} />
<Route element={<AppLayout />}>
<Route
path="unknown-session"
element={<ErrorPage message="error.invalid_session" />}
/>
<Route path="springboard" element={<Springboard />} />
<Route element={<LoadingLayerProvider />}>
{/* Sign-in */}
<Route path="sign-in">
<Route
index
element={isRegisterOnly ? <Navigate replace to="/register" /> : <SignIn />}
/>
<Route path="password" element={<SignInPassword />} />
<Route path="social/:connectorId" element={<SocialSignIn />} />
<Route element={<LoadingLayerProvider />}>
{/* Sign-in */}
<Route path="sign-in">
<Route index element={<SignIn />} />
<Route path="password" element={<SignInPassword />} />
<Route path="social/:connectorId" element={<SocialSignIn />} />
</Route>
{/* Register */}
<Route path="register">
<Route index element={<Register />} />
<Route path="password" element={<RegisterPassword />} />
</Route>
{/* Forgot password */}
<Route path="forgot-password">
<Route index element={<ForgotPassword />} />
<Route path="reset" element={<ResetPassword />} />
</Route>
{/* Passwordless verification code */}
<Route path=":flow/verification-code" element={<VerificationCode />} />
{/* Continue set up missing profile */}
<Route path="continue">
<Route path=":method" element={<Continue />} />
</Route>
{/* Social sign-in pages */}
<Route path="social">
<Route path="link/:connectorId" element={<SocialLinkAccount />} />
<Route path="landing/:connectorId" element={<SocialLanding />} />
</Route>
<Route path="callback/:connectorId" element={<Callback />} />
</Route>
{/* Register */}
<Route path="register">
<Route
index
element={isSignInOnly ? <Navigate replace to="/sign-in" /> : <Register />}
/>
<Route path="password" element={<RegisterPassword />} />
</Route>
{/* Forgot password */}
<Route path="forgot-password">
<Route index element={<ForgotPassword />} />
<Route path="reset" element={<ResetPassword />} />
</Route>
{/* Passwordless verification code */}
<Route path=":flow/verification-code" element={<VerificationCode />} />
{/* Continue set up missing profile */}
<Route path="continue">
<Route path=":method" element={<Continue />} />
</Route>
{/* Social sign-in pages */}
<Route path="social">
<Route path="link/:connectorId" element={<SocialLinkAccount />} />
<Route path="landing/:connectorId" element={<SocialLanding />} />
</Route>
<Route path="callback/:connectorId" element={<Callback />} />
<Route path="*" element={<ErrorPage />} />
</Route>
<Route path="*" element={<ErrorPage />} />
</Route>
</Routes>
</AppBoundary>
</Provider>
</Routes>
</AppBoundary>
</SettingsProvider>
</PageContextProvider>
</BrowserRouter>
);
};

View file

@ -3,8 +3,8 @@ import type { ReactNode } from 'react';
import { useContext } from 'react';
import type { TFuncKey } from 'react-i18next';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import BrandingHeader from '@/components/BrandingHeader';
import { PageContext } from '@/hooks/use-page-context';
import { layoutClassNames } from '@/utils/consts';
import { getBrandingLogoUrl } from '@/utils/logo';

View file

@ -3,7 +3,7 @@ import { Theme } from '@logto/schemas';
import color from 'color';
import { useEffect, useContext } from 'react';
import { PageContext } from '@/hooks/use-page-context';
import PageContext from '@/Providers/PageContextProvider/PageContext';
const generateLightColorLibrary = (primaryColor: color) => ({
[`--color-brand-default`]: primaryColor.hex(),

View file

@ -0,0 +1,19 @@
import { useRef, useEffect, useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
const useCustomStyle = () => {
const customCssRef = useRef(document.createElement('style'));
const { experienceSettings } = useContext(PageContext);
useEffect(() => {
document.head.append(customCssRef.current);
}, []);
useEffect(() => {
// eslint-disable-next-line @silverhand/fp/no-mutation
customCssRef.current.textContent = experienceSettings?.customCss ?? null;
}, [experienceSettings?.customCss]);
};
export default useCustomStyle;

View file

@ -0,0 +1,31 @@
import { conditionalString } from '@silverhand/essentials';
import { useEffect, useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import { setFavIcon } from '@/utils/sign-in-experience';
import * as styles from '../index.module.scss';
// TODO: replace with react-helmet
const useMetaData = () => {
const { experienceSettings, theme, platform } = useContext(PageContext);
// Set favicon
useEffect(() => {
setFavIcon(experienceSettings?.branding.favicon);
}, [experienceSettings?.branding.favicon]);
// Set Theme Mode
useEffect(() => {
document.body.classList.remove(conditionalString(styles.light), conditionalString(styles.dark));
document.body.classList.add(conditionalString(styles[theme]));
}, [theme]);
// Apply Platform Style
useEffect(() => {
document.body.classList.remove('desktop', 'mobile');
document.body.classList.add(platform === 'mobile' ? 'mobile' : 'desktop');
}, [platform]);
};
export default useMetaData;

View file

@ -1,20 +1,19 @@
import { Theme } from '@logto/schemas';
import { useEffect, useContext } from 'react';
import { PageContext } from './use-page-context';
import PageContext from '@/Providers/PageContextProvider/PageContext';
const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
const getThemeBySystemConfiguration = (): Theme =>
darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
export default function useTheme(): Theme {
const { isPreview, experienceSettings, theme, setTheme } = useContext(PageContext);
export default function useTheme() {
const { isPreview, experienceSettings, setTheme } = useContext(PageContext);
useEffect(() => {
/**
* Note:
* In preview mode, the theme of the page is controlled by the preview options and does not follow system changes.
* The `usePreview` hook changes the theme of the page by calling the `setTheme` API of the `PageContext`.
*/
if (isPreview) {
return;
@ -36,6 +35,4 @@ export default function useTheme(): Theme {
darkThemeWatchMedia.removeEventListener('change', changeTheme);
};
}, [experienceSettings, isPreview, setTheme]);
return theme;
}

View file

@ -1,39 +1,25 @@
import { conditionalString } from '@silverhand/essentials';
import type { ReactNode } from 'react';
import { useContext, useEffect } from 'react';
import type { ReactElement } from 'react';
import useColorTheme from '@/hooks/use-color-theme';
import { PageContext } from '@/hooks/use-page-context';
import useTheme from '@/hooks/use-theme';
import useColorTheme from '@/Providers/AppBoundary/hooks/use-color-theme';
import ConfirmModalProvider from '../ConfirmModalProvider';
import IframeModalProvider from '../IframeModalProvider';
import ToastProvider from '../ToastProvider';
import * as styles from './index.module.scss';
import useCustomStyle from './hooks/use-custom-style';
import useMetaData from './hooks/use-meta-data';
import useTheme from './hooks/use-theme';
type Props = {
children: ReactNode;
children: ReactElement;
};
const AppBoundary = ({ children }: Props) => {
// Set Primary Color
useColorTheme();
const theme = useTheme();
useCustomStyle();
useTheme();
const { platform } = useContext(PageContext);
// Set Theme Mode
useEffect(() => {
document.body.classList.remove(conditionalString(styles.light), conditionalString(styles.dark));
document.body.classList.add(conditionalString(styles[theme]));
}, [theme]);
// Apply Platform Style
useEffect(() => {
document.body.classList.remove('desktop', 'mobile');
document.body.classList.add(platform === 'mobile' ? 'mobile' : 'desktop');
}, [platform]);
useMetaData();
return (
<IframeModalProvider>

View file

@ -2,8 +2,8 @@ import { useContext } from 'react';
import { Outlet } from 'react-router-dom';
import { useDebouncedLoader } from 'use-debounced-loader';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import LoadingLayer from '@/components/LoadingLayer';
import { PageContext } from '@/hooks/use-page-context';
const LoadingLayerProvider = () => {
const { loading } = useContext(PageContext);

View file

@ -0,0 +1,38 @@
import { Theme } from '@logto/schemas';
import { noop } from '@silverhand/essentials';
import { createContext } from 'react';
import { isMobile } from 'react-device-detect';
import type { SignInExperienceResponse, Platform } from '@/types';
export type PageContextType = {
theme: Theme;
toast: string;
loading: boolean;
platform: Platform;
termsAgreement: boolean;
experienceSettings: SignInExperienceResponse | undefined;
isPreview: boolean;
setTheme: React.Dispatch<React.SetStateAction<Theme>>;
setToast: React.Dispatch<React.SetStateAction<string>>;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setPlatform: React.Dispatch<React.SetStateAction<Platform>>;
setTermsAgreement: React.Dispatch<React.SetStateAction<boolean>>;
setExperienceSettings: React.Dispatch<React.SetStateAction<SignInExperienceResponse | undefined>>;
};
export default createContext<PageContextType>({
toast: '',
theme: Theme.Light,
loading: false,
platform: isMobile ? 'mobile' : 'web',
termsAgreement: false,
experienceSettings: undefined,
isPreview: false,
setTheme: noop,
setToast: noop,
setLoading: noop,
setPlatform: noop,
setTermsAgreement: noop,
setExperienceSettings: noop,
});

View file

@ -0,0 +1,66 @@
import { Theme } from '@logto/schemas';
import { useState, useMemo } from 'react';
import { isMobile } from 'react-device-detect';
import { useSearchParams } from 'react-router-dom';
import type { SignInExperienceResponse, Platform } from '@/types';
import type { PageContextType } from './PageContext';
import MainContext from './PageContext';
type Props = {
children: React.ReactNode;
preset?: Partial<
Pick<
PageContextType,
| 'theme'
| 'toast'
| 'loading'
| 'platform'
| 'termsAgreement'
| 'experienceSettings'
| 'isPreview'
>
>;
};
const PageContextProvider = ({ children, preset }: Props) => {
const [searchParameters] = useSearchParams();
const [loading, setLoading] = useState(preset?.loading ?? false);
const [toast, setToast] = useState(preset?.toast ?? '');
const [theme, setTheme] = useState<Theme>(preset?.theme ?? Theme.Light);
const [platform, setPlatform] = useState<Platform>(
preset?.platform ?? (isMobile ? 'mobile' : 'web')
);
const [termsAgreement, setTermsAgreement] = useState(preset?.termsAgreement ?? false);
const [experienceSettings, setExperienceSettings] = useState<
SignInExperienceResponse | undefined
>(preset?.experienceSettings ?? undefined);
const isPreview = searchParameters.get('preview') === 'true';
const pageContext = useMemo<PageContextType>(
() => ({
theme,
toast,
loading,
platform,
termsAgreement,
experienceSettings,
isPreview,
setTheme,
setLoading,
setToast,
setPlatform,
setTermsAgreement,
setExperienceSettings,
}),
[experienceSettings, isPreview, loading, platform, termsAgreement, theme, toast]
);
return <MainContext.Provider value={pageContext}>{children}</MainContext.Provider>;
};
export default PageContextProvider;

View file

@ -1,37 +1,31 @@
import { ConnectorPlatform } from '@logto/schemas';
import { conditionalString } from '@silverhand/essentials';
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import * as styles from '@/Layout/AppLayout/index.module.scss';
import type { Context } from '@/hooks/use-page-context';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import initI18n from '@/i18n/init';
import { changeLanguage } from '@/i18n/utils';
import type { SignInExperienceResponse, PreviewConfig } from '@/types';
import type { PreviewConfig, SignInExperienceResponse } from '@/types';
import { filterPreviewSocialConnectors } from '@/utils/social-connectors';
const usePreview = (context: Context): [boolean, PreviewConfig?] => {
const PreviewProvider = () => {
const [previewConfig, setPreviewConfig] = useState<PreviewConfig>();
const { isPreview, setExperienceSettings, setPlatform, setTheme } = context;
const { setTheme, setPlatform, setExperienceSettings } = useContext(PageContext);
// Fetch the preview config
useEffect(() => {
if (!isPreview) {
return;
}
// Init i18n
const i18nInit = initI18n();
// Block pointer event
document.body.classList.add(conditionalString(styles.preview));
// Listen to the message from the ancestor window
const previewMessageHandler = async (event: MessageEvent) => {
// TODO: @simeng: we can check allowed origins via `/.well-known/endpoints`
// if (event.origin !== window.location.origin) {
// return;
// }
// #event.data should be guarded at the provider's side
if (event.data.sender === 'ac_preview') {
// #event.data should be guarded at the provider's side
// Wait for i18n to be initialized
await i18nInit;
// eslint-disable-next-line no-restricted-syntax
@ -44,17 +38,16 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
return () => {
window.removeEventListener('message', previewMessageHandler);
};
}, [isPreview]);
}, []);
// Set Experience settings
useEffect(() => {
if (!isPreview || !previewConfig) {
if (!previewConfig) {
return;
}
const {
signInExperience: { socialConnectors, ...rest },
mode,
platform,
isNative,
} = previewConfig;
@ -66,26 +59,33 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
),
};
(async () => {
setTheme(mode);
setPlatform(platform);
setExperienceSettings(experienceSettings);
})();
}, [isPreview, previewConfig, setExperienceSettings, setPlatform, setTheme]);
setExperienceSettings(experienceSettings);
}, [previewConfig, setExperienceSettings]);
// Set Theme
useEffect(() => {
if (!isPreview || !previewConfig?.language) {
return;
if (previewConfig?.mode) {
setTheme(previewConfig.mode);
}
}, [previewConfig?.mode, setTheme]);
(async () => {
await changeLanguage(previewConfig.language);
})();
}, [previewConfig?.language, isPreview]);
// Set Platform
useEffect(() => {
if (previewConfig?.platform) {
setPlatform(previewConfig.platform);
}
}, [previewConfig?.platform, setPlatform]);
return [isPreview, previewConfig];
// Set Language
useEffect(() => {
if (previewConfig?.language) {
(async () => {
await changeLanguage(previewConfig.language);
})();
}
}, [previewConfig?.language]);
return null;
};
export default usePreview;
export default PreviewProvider;

View file

@ -0,0 +1,23 @@
import { useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import PreviewProvider from '../PreviewProvider';
import SignInExperienceProvider from '../SignInExperienceProvider';
type Props = {
children: React.ReactElement;
};
const SettingsProvider = ({ children }: Props) => {
const { isPreview, experienceSettings } = useContext(PageContext);
return (
<>
{isPreview ? <PreviewProvider /> : <SignInExperienceProvider />}
{experienceSettings ? children : null}
</>
);
};
export default SettingsProvider;

View file

@ -0,0 +1,22 @@
import { useContext, useEffect } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import initI18n from '@/i18n/init';
import { getSignInExperienceSettings } from '@/utils/sign-in-experience';
const SignInExperienceProvider = () => {
const { isPreview, setExperienceSettings } = useContext(PageContext);
useEffect(() => {
(async () => {
const [settings] = await Promise.all([getSignInExperienceSettings(), initI18n()]);
// Init the page settings and render
setExperienceSettings(settings);
})();
}, [isPreview, setExperienceSettings]);
return null;
};
export default SignInExperienceProvider;

View file

@ -1,8 +1,8 @@
import type { ReactNode } from 'react';
import { useCallback, useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import Toast from '@/components/Toast';
import { PageContext } from '@/hooks/use-page-context';
type Props = {
children: ReactNode;

View file

@ -1,9 +0,0 @@
import usePageContext from '@/hooks/use-page-context';
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
const { context, Provider } = usePageContext();
return <Provider value={context}>{children}</Provider>;
};
export default ContextProvider;

View file

@ -1,7 +1,7 @@
import type { ReactElement } from 'react';
import { useContext, useEffect } from 'react';
import { PageContext } from '@/hooks/use-page-context';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import type { SignInExperienceResponse } from '@/types';
import { mockSignInExperienceSettings } from '../logto';

View file

@ -1,15 +1,24 @@
import type { Queries, queries, RenderOptions } from '@testing-library/react';
import { render } from '@testing-library/react';
import type { ReactElement } from 'react';
import { MemoryRouter } from 'react-router-dom';
import ContextProvider from './ContextProvider';
import PageContextProvider from '@/Providers/PageContextProvider';
const renderWithPageContext = <
Q extends Queries = typeof queries,
Container extends Element | DocumentFragment = HTMLElement
>(
ui: ReactElement,
memoryRouterProps: Parameters<typeof MemoryRouter>[0] = {},
options: RenderOptions<Q, Container> = {}
) => render<Q, Container>(<ContextProvider>{ui}</ContextProvider>, options);
) => {
return render<Q, Container>(
<MemoryRouter {...memoryRouterProps}>
<PageContextProvider>{ui}</PageContextProvider>
</MemoryRouter>,
options
);
};
export default renderWithPageContext;

View file

@ -1,10 +1,10 @@
import { Theme } from '@logto/schemas';
import { useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import LogtoLogtoDark from '@/assets/icons/logto-logo-dark.svg';
import LogtoLogoLight from '@/assets/icons/logto-logo-light.svg';
import LogtoLogoShadow from '@/assets/icons/logto-logo-shadow.svg';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';

View file

@ -2,8 +2,8 @@ import { Theme } from '@logto/schemas';
import classNames from 'classnames';
import { useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import { LoadingIcon } from '@/components/LoadingLayer';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';

View file

@ -1,5 +1,3 @@
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { socialConnectors } from '@/__mocks__/logto';
@ -10,9 +8,7 @@ describe('SocialSignInList', () => {
it('Display connectors', () => {
const { container } = renderWithPageContext(
<SettingsProvider>
<MemoryRouter>
<SocialSignInList socialConnectors={socialConnectors} />
</MemoryRouter>
<SocialSignInList socialConnectors={socialConnectors} />
</SettingsProvider>
);
expect(container.querySelectorAll('button')).toHaveLength(socialConnectors.length);

View file

@ -2,9 +2,9 @@ import { conditional } from '@silverhand/essentials';
import { useContext } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import TermsLinks from '@/components/TermsLinks';
import type { ModalContentRenderProps } from '@/hooks/use-confirm-modal';
import { PageContext } from '@/hooks/use-page-context';
const TermsAndPrivacyConfirmModalContent = ({ cancel }: ModalContentRenderProps) => {
const { experienceSettings } = useContext(PageContext);

View file

@ -1,6 +1,5 @@
import { SignInIdentifier } from '@logto/schemas';
import { act, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import {
@ -288,13 +287,11 @@ describe('<VerificationCode />', () => {
}));
const { container } = renderWithPageContext(
<MemoryRouter>
<VerificationCode
flow={UserFlow.continue}
identifier={SignInIdentifier.Email}
target={email}
/>
</MemoryRouter>
<VerificationCode
flow={UserFlow.continue}
identifier={SignInIdentifier.Email}
target={email}
/>
);
const inputs = container.querySelectorAll('input');
@ -323,13 +320,11 @@ describe('<VerificationCode />', () => {
}));
const { container } = renderWithPageContext(
<MemoryRouter>
<VerificationCode
flow={UserFlow.continue}
identifier={SignInIdentifier.Phone}
target={phone}
/>
</MemoryRouter>
<VerificationCode
flow={UserFlow.continue}
identifier={SignInIdentifier.Phone}
target={phone}
/>
);
const inputs = container.querySelectorAll('input');

View file

@ -1,12 +1,12 @@
import { SignInIdentifier } from '@logto/schemas';
import { t } from 'i18next';
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { useTimer } from 'react-timer-hook';
import { sendVerificationCodeApi } from '@/apis/utils';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import { PageContext } from '@/hooks/use-page-context';
import useToast from '@/hooks/use-toast';
import type { UserFlow } from '@/types';
export const timeRange = 59;
@ -23,7 +23,7 @@ const useResendVerificationCode = (
method: SignInIdentifier.Email | SignInIdentifier.Phone,
target: string
) => {
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
const { seconds, isRunning, restart } = useTimer({
autoStart: true,

View file

@ -1,7 +1,7 @@
import type { Nullable } from '@silverhand/essentials';
import { useCallback, useContext } from 'react';
import { PageContext } from '@/hooks/use-page-context';
import PageContext from '@/Providers/PageContextProvider/PageContext';
const useApi = <Args extends unknown[], Response>(api: (...args: Args) => Promise<Response>) => {
const { setLoading } = useContext(PageContext);

View file

@ -1,10 +1,10 @@
import type { LogtoErrorCode } from '@logto/phrases';
import type { RequestErrorBody } from '@logto/schemas';
import { HTTPError, TimeoutError } from 'ky';
import { useCallback, useContext } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PageContext } from '@/hooks/use-page-context';
import useToast from './use-toast';
export type ErrorHandlers = {
[key in LogtoErrorCode]?: (error: RequestErrorBody) => void | Promise<void>;
@ -15,7 +15,7 @@ export type ErrorHandlers = {
const useErrorHandler = () => {
const { t } = useTranslation();
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
const handleError = useCallback(
async (error: unknown, errorHandlers?: ErrorHandlers) => {

View file

@ -1,11 +1,11 @@
import { useEffect, useContext } from 'react';
import { useEffect } from 'react';
import { isNativeWebview } from '@/utils/native-sdk';
import { PageContext } from './use-page-context';
import useToast from './use-toast';
const useNativeMessageListener = () => {
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
// Monitor Native Error Message
useEffect(() => {

View file

@ -1,77 +0,0 @@
import { Theme } from '@logto/schemas';
import { noop } from '@silverhand/essentials';
import { useState, useMemo, createContext } from 'react';
import { isMobile } from 'react-device-detect';
import type { SignInExperienceResponse, Platform } from '@/types';
import { parseQueryParameters } from '@/utils';
export type Context = {
theme: Theme;
toast: string;
loading: boolean;
platform: Platform;
termsAgreement: boolean;
experienceSettings: SignInExperienceResponse | undefined;
isPreview: boolean;
setTheme: React.Dispatch<React.SetStateAction<Theme>>;
setToast: React.Dispatch<React.SetStateAction<string>>;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setPlatform: React.Dispatch<React.SetStateAction<Platform>>;
setTermsAgreement: React.Dispatch<React.SetStateAction<boolean>>;
setExperienceSettings: React.Dispatch<React.SetStateAction<SignInExperienceResponse | undefined>>;
};
export const PageContext = createContext<Context>({
toast: '',
theme: Theme.Light,
loading: false,
platform: isMobile ? 'mobile' : 'web',
termsAgreement: false,
experienceSettings: undefined,
isPreview: false,
setTheme: noop,
setToast: noop,
setLoading: noop,
setPlatform: noop,
setTermsAgreement: noop,
setExperienceSettings: noop,
});
const usePageContext = () => {
const [loading, setLoading] = useState(false);
const [toast, setToast] = useState('');
const [theme, setTheme] = useState<Theme>(Theme.Light);
const [platform, setPlatform] = useState<Platform>(isMobile ? 'mobile' : 'web');
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceResponse>();
const [termsAgreement, setTermsAgreement] = useState(false);
const { preview } = parseQueryParameters(window.location.search);
const isPreview = preview === 'true';
const context = useMemo(
() => ({
theme,
toast,
loading,
platform,
termsAgreement,
experienceSettings,
isPreview,
setTheme,
setLoading,
setToast,
setPlatform,
setTermsAgreement,
setExperienceSettings,
}),
[experienceSettings, isPreview, loading, platform, termsAgreement, theme, toast]
);
return {
context,
Provider: PageContext.Provider,
};
};
export default usePageContext;

View file

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { PageContext } from './use-page-context';
import PageContext from '@/Providers/PageContextProvider/PageContext';
const usePlatform = () => {
const { platform } = useContext(PageContext);

View file

@ -1,5 +1,5 @@
import { MissingProfile } from '@logto/schemas';
import { useMemo, useContext } from 'react';
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { validate } from 'superstruct';
@ -8,7 +8,7 @@ import { missingProfileErrorDataGuard } from '@/types/guard';
import { queryStringify } from '@/utils';
import type { ErrorHandlers } from './use-error-handler';
import { PageContext } from './use-page-context';
import useToast from './use-toast';
type Options = {
replace?: boolean;
@ -17,7 +17,7 @@ type Options = {
const useRequiredProfileErrorHandler = ({ replace, linkSocial }: Options = {}) => {
const navigate = useNavigate();
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
const requiredProfileErrorHandler = useMemo<ErrorHandlers>(
() => ({

View file

@ -1,10 +1,9 @@
import { SignInIdentifier } from '@logto/schemas';
import { useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import type { VerificationCodeIdentifier } from '@/types';
import { PageContext } from './use-page-context';
export const useSieMethods = () => {
const { experienceSettings } = useContext(PageContext);
const { identifiers, password, verify } = experienceSettings?.signUp ?? {};
@ -20,7 +19,6 @@ export const useSieMethods = () => {
socialConnectors: experienceSettings?.socialConnectors ?? [],
signInMode: experienceSettings?.signInMode,
forgotPassword: experienceSettings?.forgotPassword,
customCss: experienceSettings?.customCss,
customContent: experienceSettings?.customContent,
};
};

View file

@ -1,15 +1,15 @@
import { useContext, useState, useCallback } from 'react';
import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import { storeCallbackLink } from '@/utils/social-connectors';
import { PageContext } from './use-page-context';
import useToast from './use-toast';
const useSocialLandingHandler = () => {
const [loading, setLoading] = useState(true);
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
const { t } = useTranslation();
const { search } = window.location;

View file

@ -1,6 +1,6 @@
import type { RequestErrorBody } from '@logto/schemas';
import { SignInMode } from '@logto/schemas';
import { useEffect, useCallback, useContext, useMemo, useState } from 'react';
import { useEffect, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { validate } from 'superstruct';
@ -13,14 +13,14 @@ import { stateValidation } from '@/utils/social-connectors';
import useApi from './use-api';
import useErrorHandler from './use-error-handler';
import type { ErrorHandlers } from './use-error-handler';
import { PageContext } from './use-page-context';
import useRequiredProfileErrorHandler from './use-required-profile-error-handler';
import { useSieMethods } from './use-sie';
import useSocialRegister from './use-social-register';
import useTerms from './use-terms';
import useToast from './use-toast';
const useSocialSignInListener = (connectorId?: string) => {
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
const { signInMode } = useSieMethods();
const { t } = useTranslation();
const { termsValidation } = useTerms();

View file

@ -1,13 +1,13 @@
import type { ConnectorMetadata } from '@logto/schemas';
import { useCallback, useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import { getSocialAuthorizationUrl } from '@/apis/interaction';
import { getLogtoNativeSdk, isNativeWebview } from '@/utils/native-sdk';
import { generateState, storeState, buildSocialLandingUri } from '@/utils/social-connectors';
import useApi from './use-api';
import useErrorHandler from './use-error-handler';
import { PageContext } from './use-page-context';
const useSocial = () => {
const { experienceSettings, theme } = useContext(PageContext);

View file

@ -1,10 +1,10 @@
import { conditional } from '@silverhand/essentials';
import { useContext, useCallback, useMemo } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import TermsAndPrivacyConfirmModalContent from '@/containers/TermsAndPrivacy/TermsAndPrivacyConfirmModalContent';
import { useConfirmModal } from './use-confirm-modal';
import { PageContext } from './use-page-context';
const useTerms = () => {
const { termsAgreement, setTermsAgreement, experienceSettings } = useContext(PageContext);

View file

@ -0,0 +1,11 @@
import { useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
const useToast = () => {
const { toast, setToast } = useContext(PageContext);
return { toast, setToast };
};
export default useToast;

View file

@ -1,11 +1,11 @@
import { conditional } from '@silverhand/essentials';
import { useEffect, useContext, useState } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import { consent } from '@/apis/consent';
import { LoadingIcon } from '@/components/LoadingLayer';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import { PageContext } from '@/hooks/use-page-context';
import { getBrandingLogoUrl } from '@/utils/logo';
import * as styles from './index.module.scss';

View file

@ -4,10 +4,10 @@ import type { TFuncKey } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import StaticPageLayout from '@/Layout/StaticPageLayout';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import EmptyStateDark from '@/assets/icons/empty-state-dark.svg';
import EmptyState from '@/assets/icons/empty-state.svg';
import NavBar from '@/components/NavBar';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';

View file

@ -1,7 +1,6 @@
import { InteractionEvent, SignInIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { act, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { putInteraction, sendVerificationCode } from '@/apis/interaction';
@ -33,13 +32,11 @@ describe('ForgotPasswordForm', () => {
const renderForm = (defaultType: VerificationCodeIdentifier, defaultValue?: string) =>
renderWithPageContext(
<MemoryRouter>
<ForgotPasswordForm
enabledTypes={[SignInIdentifier.Email, SignInIdentifier.Phone]}
defaultType={defaultType}
defaultValue={defaultValue}
/>
</MemoryRouter>
<ForgotPasswordForm
enabledTypes={[SignInIdentifier.Email, SignInIdentifier.Phone]}
defaultType={defaultType}
defaultValue={defaultValue}
/>
);
describe.each([

View file

@ -1,7 +1,7 @@
import { SignInIdentifier } from '@logto/schemas';
import { Globals } from '@react-spring/web';
import { assert } from '@silverhand/essentials';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -24,19 +24,17 @@ jest.mock('react-router-dom', () => ({
describe('ForgotPassword', () => {
const renderPage = (settings?: SignInExperienceResponse['forgotPassword']) =>
renderWithPageContext(
<MemoryRouter>
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
forgotPassword: {
...mockSignInExperienceSettings.forgotPassword,
...settings,
},
}}
>
<ForgotPassword />
</SettingsProvider>
</MemoryRouter>
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
forgotPassword: {
...mockSignInExperienceSettings.forgotPassword,
...settings,
},
}}
>
<ForgotPassword />
</SettingsProvider>
);
beforeAll(() => {

View file

@ -1,6 +1,6 @@
import type { SignUp } from '@logto/schemas';
import { SignInMode, SignInIdentifier } from '@logto/schemas';
import { MemoryRouter } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -17,9 +17,7 @@ describe('<Register />', () => {
const renderRegisterPage = (settings?: Partial<SignInExperienceResponse>) =>
renderWithPageContext(
<SettingsProvider settings={{ ...mockSignInExperienceSettings, ...settings }}>
<MemoryRouter>
<Register />
</MemoryRouter>
<Register />
</SettingsProvider>
);
@ -59,8 +57,20 @@ describe('<Register />', () => {
expect(queryByText('action.create_account')).toBeNull();
});
test('render with sign-in only mode should return ErrorPage', () => {
const { queryByText } = renderRegisterPage({ signInMode: SignInMode.SignIn });
expect(queryByText('description.not_found')).not.toBeNull();
test('render with sign-in only mode should should redirect to the SignIn page', () => {
const { queryByText } = renderWithPageContext(
<SettingsProvider
settings={{ ...mockSignInExperienceSettings, signInMode: SignInMode.SignIn }}
>
<Routes>
<Route path="sign-in" element={<div>sign-in</div>} />
<Route path="register" element={<Register />} />
</Routes>
</SettingsProvider>,
{
initialEntries: ['/register'],
}
);
expect(queryByText('sign-in')).not.toBeNull();
});
});

View file

@ -1,5 +1,6 @@
import { SignInMode } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import { Navigate } from 'react-router-dom';
import LandingPageLayout from '@/Layout/LandingPageLayout';
import Divider from '@/components/Divider';
@ -17,10 +18,14 @@ const Register = () => {
const { signUpMethods, socialConnectors, signInMode, signInMethods } = useSieMethods();
const { t } = useTranslation();
if (!signInMode || signInMode === SignInMode.SignIn) {
if (!signInMode) {
return <ErrorPage />;
}
if (signInMode === SignInMode.SignIn) {
return <Navigate to="/sign-in" />;
}
return (
<LandingPageLayout title="description.create_your_account">
{signUpMethods.length > 0 && (

View file

@ -1,5 +1,5 @@
import { act, waitFor, fireEvent } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { Routes, Route } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { setUserPassword } from '@/apis/interaction';
@ -20,11 +20,10 @@ jest.mock('@/apis/interaction', () => ({
describe('ForgotPassword', () => {
it('render forgot-password page properly', () => {
const { queryByText, container } = renderWithPageContext(
<MemoryRouter initialEntries={['/forgot-password']}>
<Routes>
<Route path="/forgot-password" element={<ResetPassword />} />
</Routes>
</MemoryRouter>
<Routes>
<Route path="/forgot-password" element={<ResetPassword />} />
</Routes>,
{ initialEntries: ['/forgot-password'] }
);
expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();

View file

@ -1,4 +1,4 @@
import { useMemo, useState, useContext, useCallback } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@ -7,12 +7,12 @@ import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useErrorHandler from '@/hooks/use-error-handler';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import { PageContext } from '@/hooks/use-page-context';
import useToast from '@/hooks/use-toast';
const useResetPassword = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
const { show } = useConfirmModal();
const [errorMessage, setErrorMessage] = useState<string>();

View file

@ -2,7 +2,6 @@ import type { SignIn } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { fireEvent, act, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { mockSignInMethodSettingsTestCases } from '@/__mocks__/logto';
@ -34,11 +33,7 @@ const email = 'foo@email.com';
const phone = '8573333333';
const renderForm = (signInMethods: SignIn['methods']) =>
renderWithPageContext(
<MemoryRouter>
<IdentifierSignInForm signInMethods={signInMethods} />
</MemoryRouter>
);
renderWithPageContext(<IdentifierSignInForm signInMethods={signInMethods} />);
describe('IdentifierSignInForm', () => {
afterEach(() => {

View file

@ -2,7 +2,6 @@ import { SignInIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { fireEvent, waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -35,9 +34,7 @@ describe('UsernamePasswordSignInForm', () => {
) =>
renderWithPageContext(
<SettingsProvider settings={{ ...mockSignInExperienceSettings, ...settings }}>
<MemoryRouter>
<PasswordSignInForm signInMethods={signInMethods} />
</MemoryRouter>
<PasswordSignInForm signInMethods={signInMethods} />
</SettingsProvider>
);

View file

@ -1,5 +1,5 @@
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { MemoryRouter } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -11,13 +11,16 @@ jest.mock('i18next', () => ({
language: 'en',
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => jest.fn(),
}));
describe('<SignIn />', () => {
const renderSignIn = (settings?: Partial<typeof mockSignInExperienceSettings>) =>
renderWithPageContext(
<SettingsProvider settings={{ ...mockSignInExperienceSettings, ...settings }}>
<MemoryRouter>
<SignIn />
</MemoryRouter>
<SignIn />
</SettingsProvider>
);
@ -76,9 +79,7 @@ describe('<SignIn />', () => {
test('renders with social as primary', async () => {
const { container, queryByText } = renderWithPageContext(
<SettingsProvider settings={{ ...mockSignInExperienceSettings, signIn: { methods: [] } }}>
<MemoryRouter>
<SignIn />
</MemoryRouter>
<SignIn />
</SettingsProvider>
);
@ -90,17 +91,21 @@ describe('<SignIn />', () => {
expect(queryByText('description.privacy_policy')).not.toBeNull();
});
test('render with register only mode should return ErrorPage', () => {
test('render with register only mode should redirect to the Register page', () => {
const { queryByText } = renderWithPageContext(
<SettingsProvider
settings={{ ...mockSignInExperienceSettings, signInMode: SignInMode.Register }}
>
<MemoryRouter>
<SignIn />
</MemoryRouter>
</SettingsProvider>
<Routes>
<Route path="sign-in" element={<SignIn />} />
<Route path="register" element={<div>Register</div>} />
</Routes>
</SettingsProvider>,
{
initialEntries: ['/sign-in'],
}
);
expect(queryByText('description.not_found')).not.toBeNull();
expect(queryByText('Register')).not.toBeNull();
});
});

View file

@ -1,5 +1,6 @@
import { SignInMode } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import { Navigate } from 'react-router-dom';
import LandingPageLayout from '@/Layout/LandingPageLayout';
import Divider from '@/components/Divider';
@ -17,10 +18,14 @@ const SignIn = () => {
const { signInMethods, signUpMethods, socialConnectors, signInMode } = useSieMethods();
const { t } = useTranslation();
if (!signInMode || signInMode === SignInMode.Register) {
if (!signInMode) {
return <ErrorPage />;
}
if (signInMode === SignInMode.Register) {
return <Navigate to="/register" />;
}
return (
<LandingPageLayout title="description.sign_in_to_your_account">
<Main signInMethods={signInMethods} socialConnectors={socialConnectors} />

View file

@ -1,9 +1,9 @@
import { useContext, useEffect } from 'react';
import { useEffect } from 'react';
import SwitchIcon from '@/assets/icons/switch-icon.svg';
import TextLink from '@/components/TextLink';
import { PageContext } from '@/hooks/use-page-context';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import useToast from '@/hooks/use-toast';
import type { VerificationCodeIdentifier } from '@/types';
import { UserFlow } from '@/types';
@ -14,7 +14,7 @@ type Props = {
};
const VerificationCodeLink = ({ className, identifier, value }: Props) => {
const { setToast } = useContext(PageContext);
const { setToast } = useToast();
const { errorMessage, clearErrorMessage, onSubmit } = useSendVerificationCode(
UserFlow.signIn,

View file

@ -1,5 +1,5 @@
import { SignInIdentifier } from '@logto/schemas';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -20,16 +20,14 @@ describe('SignInPassword', () => {
const renderPasswordSignInPage = (settings?: Partial<typeof mockSignInExperienceSettings>) =>
renderWithPageContext(
<MemoryRouter>
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
...settings,
}}
>
<SignInPassword />
</SettingsProvider>
</MemoryRouter>
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
...settings,
}}
>
<SignInPassword />
</SettingsProvider>
);
beforeEach(() => {

View file

@ -1,5 +1,5 @@
import { waitFor } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -31,12 +31,11 @@ describe(`SocialLanding Page`, () => {
renderWithPageContext(
<SettingsProvider>
<MemoryRouter initialEntries={['/social/landing/github']}>
<Routes>
<Route path="/social/landing/:connectorId" element={<SocialLanding />} />
</Routes>
</MemoryRouter>
</SettingsProvider>
<Routes>
<Route path="/social/landing/:connectorId" element={<SocialLanding />} />
</Routes>
</SettingsProvider>,
{ initialEntries: ['/social/landing/github'] }
);
await waitFor(() => {

View file

@ -1,5 +1,5 @@
import { SignInIdentifier } from '@logto/schemas';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -18,12 +18,11 @@ describe('SocialRegister', () => {
it('render', () => {
const { queryByText } = renderWithPageContext(
<SettingsProvider>
<MemoryRouter initialEntries={['/social/link/github']}>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</MemoryRouter>
</SettingsProvider>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</SettingsProvider>,
{ initialEntries: ['/social/link/github'] }
);
expect(queryByText('description.bind_account_title')).not.toBeNull();
expect(queryByText('description.social_create_account')).not.toBeNull();
@ -41,12 +40,11 @@ describe('SocialRegister', () => {
},
}}
>
<MemoryRouter initialEntries={['/social/link/github']}>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</MemoryRouter>
</SettingsProvider>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</SettingsProvider>,
{ initialEntries: ['/social/link/github'] }
);
expect(queryByText('description.link_email')).not.toBeNull();
expect(queryByText('description.social_link_email')).not.toBeNull();
@ -64,12 +62,11 @@ describe('SocialRegister', () => {
},
}}
>
<MemoryRouter initialEntries={['/social/link/github']}>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</MemoryRouter>
</SettingsProvider>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</SettingsProvider>,
{ initialEntries: ['/social/link/github'] }
);
expect(queryByText('description.link_phone')).not.toBeNull();
expect(queryByText('description.social_link_phone')).not.toBeNull();
@ -87,12 +84,11 @@ describe('SocialRegister', () => {
},
}}
>
<MemoryRouter initialEntries={['/social/link/github']}>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</MemoryRouter>
</SettingsProvider>
<Routes>
<Route path="/social/link/:connectorId" element={<SocialRegister />} />
</Routes>
</SettingsProvider>,
{ initialEntries: ['/social/link/github'] }
);
expect(queryByText('description.link_email_or_phone')).not.toBeNull();
expect(queryByText('description.social_link_email_or_phone')).not.toBeNull();

View file

@ -1,5 +1,5 @@
import { waitFor } from '@testing-library/react';
import { MemoryRouter, Route, Routes, useSearchParams } from 'react-router-dom';
import { Route, Routes, useSearchParams } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -38,12 +38,11 @@ describe('SocialCallbackPage with code', () => {
renderWithPageContext(
<SettingsProvider>
<MemoryRouter initialEntries={['/sign-in/social/github']}>
<Routes>
<Route path="/sign-in/social/:connectorId" element={<SocialCallback />} />
</Routes>
</MemoryRouter>
</SettingsProvider>
<Routes>
<Route path="/sign-in/social/:connectorId" element={<SocialCallback />} />
</Routes>
</SettingsProvider>,
{ initialEntries: ['/sign-in/social/github'] }
);
await waitFor(() => {

View file

@ -1,4 +1,4 @@
import { Routes, Route, MemoryRouter } from 'react-router-dom';
import { Routes, Route } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
@ -15,13 +15,12 @@ jest.mock('react-router-dom', () => ({
describe('VerificationCode Page', () => {
it('render properly', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/sign-in/verification-code']}>
<SettingsProvider>
<Routes>
<Route path="/:flow/verification-code" element={<VerificationCode />} />
</Routes>
</SettingsProvider>
</MemoryRouter>
<SettingsProvider>
<Routes>
<Route path="/:flow/verification-code" element={<VerificationCode />} />
</Routes>
</SettingsProvider>,
{ initialEntries: ['/sign-in/verification-code'] }
);
expect(queryByText('action.enter_passcode')).not.toBeNull();
@ -30,11 +29,10 @@ describe('VerificationCode Page', () => {
it('render with invalid flow', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/social/verification-code']}>
<Routes>
<Route path="/:flow/verification-code" element={<VerificationCode />} />
</Routes>
</MemoryRouter>
<Routes>
<Route path="/:flow/verification-code" element={<VerificationCode />} />
</Routes>,
{ initialEntries: ['/social/verification-code'] }
);
expect(queryByText('action.enter_passcode')).toBeNull();