0
Fork 0
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:
simeng-li 2023-04-04 15:41:07 +08:00 committed by GitHub
parent 7915681b5a
commit dfef709b98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 85 additions and 130 deletions

View file

@ -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;

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
</>
);
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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
});
});

View file

@ -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 ?? {};

View file

@ -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,
}}

View file

@ -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 */
};