mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(ui): leverage react-helmet to manage headers (#3670)
* refactor(ui): leverage react-helmet to manage headers leverage react-helmet to manage headers, html and body attrs * fix(ui): renaming address some comments, rename the data-color-mode and provide a general useSignInExperience hook
This commit is contained in:
parent
7915681b5a
commit
dfef709b98
14 changed files with 85 additions and 130 deletions
|
@ -1,16 +1,16 @@
|
|||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import { useSignInExperience } from '@/hooks/use-sie';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CustomContent = ({ className }: Props) => {
|
||||
const { customContent } = useSieMethods();
|
||||
const signInExperience = useSignInExperience();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const customHtml = customContent?.[pathname];
|
||||
const customHtml = signInExperience?.customContent[pathname];
|
||||
|
||||
if (!customHtml) {
|
||||
return null;
|
||||
|
|
49
packages/ui/src/Providers/AppBoundary/AppMeta.tsx
Normal file
49
packages/ui/src/Providers/AppBoundary/AppMeta.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { conditionalString } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import i18next from 'i18next';
|
||||
import { useContext } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
||||
import defaultAppleTouchLogo from '@/assets/apple-touch-icon.png';
|
||||
import defaultFavicon from '@/assets/favicon.png';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
/**
|
||||
* User React Helmet to manage html and body attributes
|
||||
* @see https://github.com/nfl/react-helmet
|
||||
*
|
||||
* - lang: set html lang attribute
|
||||
* - data-theme: set html data-theme attribute
|
||||
* - favicon: set favicon
|
||||
* - apple-touch-icon: set apple touch icon
|
||||
* - body class: set platform body class
|
||||
* - body class: set theme body class
|
||||
* - custom css: set custom css style tag
|
||||
*/
|
||||
|
||||
const AppMeta = () => {
|
||||
const { experienceSettings, theme, platform } = useContext(PageContext);
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
<html lang={i18next.language} data-theme={theme} />
|
||||
<link rel="shortcut icon" href={experienceSettings?.branding.favicon ?? defaultFavicon} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href={experienceSettings?.branding.favicon ?? defaultAppleTouchLogo}
|
||||
sizes="180x180"
|
||||
/>
|
||||
{experienceSettings?.customCss && <style>{experienceSettings.customCss}</style>}
|
||||
<body
|
||||
className={classNames(
|
||||
platform === 'mobile' ? 'mobile' : 'desktop',
|
||||
conditionalString(styles[theme])
|
||||
)}
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppMeta;
|
|
@ -1,19 +0,0 @@
|
|||
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;
|
|
@ -1,31 +0,0 @@
|
|||
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,14 +1,12 @@
|
|||
import type { ReactElement } from 'react';
|
||||
|
||||
import useColorTheme from '@/Providers/AppBoundary/hooks/use-color-theme';
|
||||
import useColorTheme from '@/Providers/AppBoundary/use-color-theme';
|
||||
|
||||
import ConfirmModalProvider from '../ConfirmModalProvider';
|
||||
import IframeModalProvider from '../IframeModalProvider';
|
||||
import ToastProvider from '../ToastProvider';
|
||||
|
||||
import useCustomStyle from './hooks/use-custom-style';
|
||||
import useMetaData from './hooks/use-meta-data';
|
||||
import useTheme from './hooks/use-theme';
|
||||
import AppMeta from './AppMeta';
|
||||
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
|
@ -16,17 +14,16 @@ type Props = {
|
|||
|
||||
const AppBoundary = ({ children }: Props) => {
|
||||
useColorTheme();
|
||||
useCustomStyle();
|
||||
useTheme();
|
||||
|
||||
useMetaData();
|
||||
|
||||
return (
|
||||
<IframeModalProvider>
|
||||
<ConfirmModalProvider>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
</ConfirmModalProvider>
|
||||
</IframeModalProvider>
|
||||
<>
|
||||
<AppMeta />
|
||||
<IframeModalProvider>
|
||||
<ConfirmModalProvider>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
</ConfirmModalProvider>
|
||||
</IframeModalProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useContext } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
|
||||
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
||||
|
||||
import PreviewProvider from '../PreviewProvider';
|
||||
import SignInExperienceProvider from '../SignInExperienceProvider';
|
||||
import usePreview from './use-preview';
|
||||
import useSignInExperience from './use-sign-in-experience';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactElement;
|
||||
|
@ -12,12 +12,11 @@ type Props = {
|
|||
const SettingsProvider = ({ children }: Props) => {
|
||||
const { isPreview, experienceSettings } = useContext(PageContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPreview ? <PreviewProvider /> : <SignInExperienceProvider />}
|
||||
{experienceSettings ? children : null}
|
||||
</>
|
||||
);
|
||||
const usePageLoad = useMemo(() => (isPreview ? usePreview : useSignInExperience), [isPreview]);
|
||||
|
||||
usePageLoad();
|
||||
|
||||
return experienceSettings ? children : null;
|
||||
};
|
||||
|
||||
export default SettingsProvider;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { changeLanguage } from '@/i18n/utils';
|
|||
import type { PreviewConfig, SignInExperienceResponse } from '@/types';
|
||||
import { filterPreviewSocialConnectors } from '@/utils/social-connectors';
|
||||
|
||||
const PreviewProvider = () => {
|
||||
const usePreview = () => {
|
||||
const [previewConfig, setPreviewConfig] = useState<PreviewConfig>();
|
||||
const { setTheme, setPlatform, setExperienceSettings } = useContext(PageContext);
|
||||
|
||||
|
@ -84,8 +84,6 @@ const PreviewProvider = () => {
|
|||
})();
|
||||
}
|
||||
}, [previewConfig?.language]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PreviewProvider;
|
||||
export default usePreview;
|
|
@ -4,9 +4,13 @@ import PageContext from '@/Providers/PageContextProvider/PageContext';
|
|||
import initI18n from '@/i18n/init';
|
||||
import { getSignInExperienceSettings } from '@/utils/sign-in-experience';
|
||||
|
||||
const SignInExperienceProvider = () => {
|
||||
import useTheme from './use-theme';
|
||||
|
||||
const useSignInExperience = () => {
|
||||
const { isPreview, setExperienceSettings } = useContext(PageContext);
|
||||
|
||||
useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const [settings] = await Promise.all([getSignInExperienceSettings(), initI18n()]);
|
||||
|
@ -15,8 +19,6 @@ const SignInExperienceProvider = () => {
|
|||
setExperienceSettings(settings);
|
||||
})();
|
||||
}, [isPreview, setExperienceSettings]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SignInExperienceProvider;
|
||||
export default useSignInExperience;
|
|
@ -11,14 +11,6 @@ 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.
|
||||
*/
|
||||
if (isPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!experienceSettings?.color.isDarkModeEnabled) {
|
||||
return;
|
||||
}
|
|
@ -244,8 +244,6 @@ describe('<VerificationCode />', () => {
|
|||
verificationCode: '111111',
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: @simeng test exception flow to fulfill the password
|
||||
});
|
||||
|
||||
it('fire phone forgot-password validate verification code event', async () => {
|
||||
|
@ -275,8 +273,6 @@ describe('<VerificationCode />', () => {
|
|||
verificationCode: '111111',
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: @simeng test exception flow to fulfill the password
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { SignInIdentifier } from '@logto/schemas';
|
|||
import { useContext } from 'react';
|
||||
|
||||
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
||||
import type { VerificationCodeIdentifier } from '@/types';
|
||||
import { type VerificationCodeIdentifier } from '@/types';
|
||||
|
||||
export const useSieMethods = () => {
|
||||
const { experienceSettings } = useContext(PageContext);
|
||||
|
@ -19,10 +19,15 @@ export const useSieMethods = () => {
|
|||
socialConnectors: experienceSettings?.socialConnectors ?? [],
|
||||
signInMode: experienceSettings?.signInMode,
|
||||
forgotPassword: experienceSettings?.forgotPassword,
|
||||
customContent: experienceSettings?.customContent,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSignInExperience = () => {
|
||||
const { experienceSettings } = useContext(PageContext);
|
||||
|
||||
return experienceSettings;
|
||||
};
|
||||
|
||||
export const useForgotPasswordSettings = () => {
|
||||
const { experienceSettings } = useContext(PageContext);
|
||||
const { forgotPassword } = experienceSettings ?? {};
|
||||
|
|
|
@ -44,7 +44,6 @@ const VerificationCode = () => {
|
|||
title={`description.verify_${identifier}`}
|
||||
description="description.enter_passcode"
|
||||
descriptionProps={{
|
||||
// TODO: @simeng consider align the phrase key to 'phone'
|
||||
address: t(`description.${identifier === 'email' ? 'email' : 'phone_number'}`),
|
||||
target: identifier === 'phone' ? formatPhoneNumberWithCountryCallingCode(value) : value,
|
||||
}}
|
||||
|
|
|
@ -7,8 +7,6 @@ import { SignInIdentifier } from '@logto/schemas';
|
|||
import i18next from 'i18next';
|
||||
|
||||
import { getSignInExperience } from '@/apis/settings';
|
||||
import defaultAppleTouchLogo from '@/assets/apple-touch-icon.png';
|
||||
import defaultFavicon from '@/assets/favicon.png';
|
||||
import type { SignInExperienceResponse } from '@/types';
|
||||
import { filterSocialConnectors } from '@/utils/social-connectors';
|
||||
|
||||
|
@ -55,33 +53,3 @@ export const parseHtmlTitle = (path: string) => {
|
|||
|
||||
return 'Logto';
|
||||
};
|
||||
|
||||
export const setFavIcon = (icon?: string) => {
|
||||
const iconLink = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
||||
const appleTouchIconLink = document.querySelector<HTMLLinkElement>(
|
||||
'link[rel="apple-touch-icon"]'
|
||||
);
|
||||
|
||||
/* eslint-disable @silverhand/fp/no-mutation */
|
||||
|
||||
if (iconLink) {
|
||||
iconLink.href = icon ?? defaultFavicon;
|
||||
} else {
|
||||
const favIconLink = document.createElement('link');
|
||||
favIconLink.rel = 'shortcut icon';
|
||||
favIconLink.href = icon ?? defaultFavicon;
|
||||
document.querySelectorAll('head')[0]?.append(favIconLink);
|
||||
}
|
||||
|
||||
if (appleTouchIconLink) {
|
||||
appleTouchIconLink.href = icon ?? defaultAppleTouchLogo;
|
||||
} else {
|
||||
const appleTouchIconLink = document.createElement('link');
|
||||
appleTouchIconLink.rel = 'apple-touch-icon';
|
||||
appleTouchIconLink.href = icon ?? defaultAppleTouchLogo;
|
||||
appleTouchIconLink.setAttribute('sizes', '180x180');
|
||||
document.querySelectorAll('head')[0]?.append(appleTouchIconLink);
|
||||
}
|
||||
|
||||
/* eslint-enable @silverhand/fp/no-mutation */
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue