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:
parent
e6394b07b3
commit
fc08fb5575
53 changed files with 495 additions and 441 deletions
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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(),
|
|
@ -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;
|
31
packages/ui/src/Providers/AppBoundary/hooks/use-meta-data.ts
Normal file
31
packages/ui/src/Providers/AppBoundary/hooks/use-meta-data.ts
Normal 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;
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
});
|
66
packages/ui/src/Providers/PageContextProvider/index.tsx
Normal file
66
packages/ui/src/Providers/PageContextProvider/index.tsx
Normal 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;
|
|
@ -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;
|
23
packages/ui/src/Providers/SettingsProvider/index.tsx
Normal file
23
packages/ui/src/Providers/SettingsProvider/index.tsx
Normal 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;
|
22
packages/ui/src/Providers/SignInExperienceProvider/index.tsx
Normal file
22
packages/ui/src/Providers/SignInExperienceProvider/index.tsx
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
|
@ -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>(
|
||||
() => ({
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
11
packages/ui/src/hooks/use-toast.ts
Normal file
11
packages/ui/src/hooks/use-toast.ts
Normal 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;
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue