0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-24 22:05:56 -05:00

refactor(ui): use postMessage on preview (#929)

* refactor(ui): use postMessage on preview

use postMessage on preview

* fix(ui): cr fix

cr fix

* fix(ui): cr fix

cr fix

* fix(ui): cr fix

cr fix
This commit is contained in:
simeng-li 2022-05-24 11:38:20 +08:00 committed by GitHub
parent 10d30ee309
commit cd18a7a046
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 118 additions and 81 deletions

View file

@ -2,7 +2,7 @@ import { Language } from '@logto/phrases';
import { AppearanceMode, ConnectorDTO, ConnectorMetadata, SignInExperience } from '@logto/schemas';
import classNames from 'classnames';
import dayjs from 'dayjs';
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
@ -25,10 +25,11 @@ const Preview = ({ signInExperience, className }: Props) => {
const [mode, setMode] = useState<AppearanceMode>(AppearanceMode.LightMode);
const [platform, setPlatform] = useState<'web' | 'mobile'>('mobile');
const { data: allConnectors } = useSWR<ConnectorDTO[], RequestError>('/api/connectors');
const previewRef = useRef<HTMLIFrameElement>(null);
const config = useMemo(() => {
if (!allConnectors) {
return '';
return;
}
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
@ -43,19 +44,40 @@ const Preview = ({ signInExperience, className }: Props) => {
[]
);
return encodeURIComponent(
JSON.stringify({
signInExperience: {
...signInExperience,
socialConnectors,
},
language,
mode,
platform,
})
);
return {
signInExperience: {
...signInExperience,
socialConnectors,
},
language,
mode,
platform,
};
}, [allConnectors, language, mode, platform, signInExperience]);
const postPreviewMessage = useCallback(() => {
if (!config) {
return;
}
previewRef.current?.contentWindow?.postMessage(
{ sender: 'ac_preview', config },
window.location.origin
);
}, [config]);
useEffect(() => {
postPreviewMessage();
const iframe = previewRef.current;
iframe?.addEventListener('load', postPreviewMessage);
return () => {
iframe?.removeEventListener('load', postPreviewMessage);
};
}, [postPreviewMessage]);
return (
<Card className={classNames(styles.preview, className)}>
<div className={styles.header}>
@ -109,7 +131,7 @@ const Preview = ({ signInExperience, className }: Props) => {
<img src={TopInfoImage} />
</div>
)}
<iframe src={`/sign-in?config=${config}&preview=true`} />
<iframe ref={previewRef} src="/sign-in?preview=true" />
</div>
</div>
</Card>

View file

@ -0,0 +1,9 @@
.preview {
pointer-events: none;
user-select: none;
main {
pointer-events: none;
user-select: none;
}
}

View file

@ -1,6 +1,8 @@
import { conditionalString } from '@silverhand/essentials';
import React, { useEffect } from 'react';
import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
import * as styles from './App.module.scss';
import AppContent from './components/AppContent';
import usePageContext from './hooks/use-page-context';
import usePreview from './hooks/use-preview';
@ -13,24 +15,26 @@ import Register from './pages/Register';
import SecondarySignIn from './pages/SecondarySignIn';
import SignIn from './pages/SignIn';
import SocialRegister from './pages/SocialRegister';
import getSignInExperienceSettings, {
parseSignInExperienceSettings,
} from './utils/sign-in-experience';
import getSignInExperienceSettings from './utils/sign-in-experience';
import './scss/normalized.scss';
const App = () => {
const { context, Provider } = usePageContext();
const { experienceSettings, setLoading, setExperienceSettings } = context;
const [isPreview, previewSettings] = usePreview();
const [isPreview] = usePreview(context);
useEffect(() => {
if (isPreview) {
document.body.classList.add(conditionalString(styles.preview));
return;
}
(async () => {
setLoading(true);
const settings = previewSettings
? parseSignInExperienceSettings(previewSettings.signInExperience)
: await getSignInExperienceSettings();
const settings = await getSignInExperienceSettings();
// Note: i18n must be initialized ahead of global experience settings
await initI18n(settings.languageInfo);
@ -39,7 +43,7 @@ const App = () => {
setLoading(false);
})();
}, [isPreview, previewSettings, setExperienceSettings, setLoading]);
}, [isPreview, setExperienceSettings, setLoading]);
if (!experienceSettings) {
return null;
@ -47,7 +51,7 @@ const App = () => {
return (
<Provider value={context}>
<AppContent mode={previewSettings?.mode} platform={previewSettings?.platform}>
<AppContent>
<BrowserRouter>
<Routes>
{/* always keep route path with param as the last one */}

View file

@ -1,4 +1,3 @@
import { AppearanceMode } from '@logto/schemas';
import { conditionalString } from '@silverhand/essentials';
import React, { ReactNode, useEffect, useCallback, useContext } from 'react';
import { useDebouncedLoader } from 'use-debounced-loader';
@ -7,20 +6,16 @@ import LoadingLayer from '@/components/LoadingLayer';
import Toast from '@/components/Toast';
import { PageContext } from '@/hooks/use-page-context';
import useTheme from '@/hooks/use-theme';
import { Platform } from '@/types';
import * as styles from './index.module.scss';
export type Props = {
children: ReactNode;
mode?: AppearanceMode;
platform?: Platform;
};
const AppContent = ({ children, mode, platform: platformOverwrite }: Props) => {
const theme = useTheme(mode);
const { toast, loading, platform, setPlatform, setToast, experienceSettings } =
useContext(PageContext);
const AppContent = ({ children }: Props) => {
const theme = useTheme();
const { toast, loading, platform, setToast, experienceSettings } = useContext(PageContext);
const debouncedLoading = useDebouncedLoader(loading);
// Prevent internal eventListener rebind
@ -45,13 +40,6 @@ const AppContent = ({ children, mode, platform: platformOverwrite }: Props) => {
);
}, [experienceSettings]);
// Set Platform
useEffect(() => {
if (platformOverwrite) {
setPlatform(platformOverwrite);
}
}, [platformOverwrite, setPlatform]);
// Set Theme Mode
useEffect(() => {
document.body.classList.remove(conditionalString(styles.light), conditionalString(styles.dark));

View file

@ -5,7 +5,6 @@ import { useState, useCallback, useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { PageContext } from '@/hooks/use-page-context';
import usePreview from '@/hooks/use-preview';
type UseApi<T extends any[], U> = {
result?: U;
@ -27,7 +26,6 @@ function useApi<Args extends any[], Response>(
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const [error, setError] = useState<RequestErrorBody>();
const [result, setResult] = useState<Response>();
const [isPreview] = usePreview();
const { setLoading, setToast } = useContext(PageContext);
@ -53,10 +51,6 @@ function useApi<Args extends any[], Response>(
const run = useCallback(
async (...args: Args) => {
if (isPreview) {
return;
}
setLoading(true);
// eslint-disable-next-line unicorn/no-useless-undefined
setError(undefined);
@ -72,7 +66,7 @@ function useApi<Args extends any[], Response>(
setLoading(false);
}
},
[isPreview, api, parseError, setLoading]
[api, parseError, setLoading]
);
useEffect(() => {

View file

@ -3,7 +3,7 @@ import { isMobile } from 'react-device-detect';
import { SignInExperienceSettings, Platform, Theme } from '@/types';
type Context = {
export type Context = {
theme: Theme;
toast: string;
loading: boolean;

View file

@ -1,48 +1,75 @@
import { Language } from '@logto/phrases';
import { AppearanceMode } from '@logto/schemas';
import { useMemo } from 'react';
import i18next from 'i18next';
import { useEffect, useState } from 'react';
import { Context } from '@/hooks/use-page-context';
import initI18n from '@/i18n/init';
import { SignInExperienceSettingsResponse } from '@/types';
import { parseQueryParameters } from '@/utils';
import { parseSignInExperienceSettings } from '@/utils/sign-in-experience';
type PreviewConfig = {
signInExperience: SignInExperienceSettingsResponse;
language: Language;
mode: AppearanceMode;
mode: AppearanceMode.LightMode | AppearanceMode.DarkMode;
platform: 'web' | 'mobile';
};
const usePreview = (): [boolean, PreviewConfig?] => {
const { preview, config } = parseQueryParameters(window.location.search);
const usePreview = (context: Context): [boolean, PreviewConfig?] => {
const [previewConfig, setPreviewConfig] = useState<PreviewConfig>();
const { setTheme, setExperienceSettings, setPlatform } = context;
const previewConfig = useMemo(() => {
if (!preview || !config) {
const { preview } = parseQueryParameters(window.location.search);
const isPreview = preview === 'true';
useEffect(() => {
if (!isPreview) {
return;
}
try {
const {
signInExperience: { languageInfo, ...rest },
language,
mode,
platform,
} = JSON.parse(decodeURIComponent(config)) as PreviewConfig;
// Init i18n
void initI18n();
// Overwrite languageInfo
const settings: SignInExperienceSettingsResponse = {
...rest,
languageInfo: {
...languageInfo,
fixedLanguage: language,
autoDetect: false,
},
};
const previewMessageHandler = (event: MessageEvent) => {
if (event.origin !== window.location.origin) {
return;
}
return { signInExperience: settings, language, mode, platform };
} catch {}
}, [config, preview]);
if (event.data.sender === 'ac_preview') {
setPreviewConfig(event.data.config as PreviewConfig);
}
};
return [Boolean(preview), previewConfig];
window.addEventListener('message', previewMessageHandler);
return () => {
window.removeEventListener('message', previewMessageHandler);
};
}, [isPreview]);
useEffect(() => {
if (!isPreview || !previewConfig) {
return;
}
const { signInExperience, language, mode, platform } = previewConfig;
const experienceSettings = parseSignInExperienceSettings(signInExperience);
void i18next.changeLanguage(language);
setTheme(mode);
setPlatform(platform);
setExperienceSettings({
...experienceSettings,
branding: {
...experienceSettings.branding,
isDarkModeEnabled: false, // Disable theme mode auto detection on preview
},
});
}, [isPreview, previewConfig, setExperienceSettings, setPlatform, setTheme]);
return [isPreview, previewConfig];
};
export default usePreview;

View file

@ -1,4 +1,3 @@
import { AppearanceMode } from '@logto/schemas';
import { useEffect, useContext } from 'react';
import { Theme } from '@/types';
@ -8,16 +7,10 @@ import { PageContext } from './use-page-context';
const darkThemeWatchMedia = window.matchMedia('(prefers-color-scheme: dark)');
const getThemeBySystemConfiguration = (): Theme => (darkThemeWatchMedia.matches ? 'dark' : 'light');
export default function useTheme(mode: AppearanceMode = AppearanceMode.SyncWithSystem): Theme {
export default function useTheme(): Theme {
const { experienceSettings, theme, setTheme } = useContext(PageContext);
useEffect(() => {
if (mode !== AppearanceMode.SyncWithSystem) {
setTheme(mode);
return;
}
if (!experienceSettings?.branding.isDarkModeEnabled) {
return;
}
@ -33,7 +26,7 @@ export default function useTheme(mode: AppearanceMode = AppearanceMode.SyncWithS
return () => {
darkThemeWatchMedia.removeEventListener('change', changeTheme);
};
}, [experienceSettings, mode, setTheme]);
}, [experienceSettings, setTheme]);
return theme;
}