diff --git a/packages/console/src/components/SignInExperiencePreview/index.module.scss b/packages/console/src/components/SignInExperiencePreview/index.module.scss new file mode 100644 index 000000000..6d8df2c8c --- /dev/null +++ b/packages/console/src/components/SignInExperiencePreview/index.module.scss @@ -0,0 +1,92 @@ +@use '@/scss/underscore' as _; + +.preview { + background: var(--color-surface-variant); + + iframe { + border: none; + } + + &.web { + .device { + width: 480px; + height: 380px; + position: relative; + background: var(--color-surface-1); + margin: 0 auto; + + iframe { + width: 960px; + height: 760px; + transform: scale(0.5); + position: absolute; + top: -190px; + left: -240px; + background: var(--color-surface-1); + } + } + } + + &.mobile { + padding: _.unit(10) 0; + height: 500px; + position: relative; + + .deviceWrapper { + width: 390px; + height: 450px; + margin: 0 auto; + transform: scale(0.5); + transform-origin: top center; + + .device { + border-radius: 26px; + overflow: hidden; + + .topBar { + display: flex; + align-items: center; + padding: _.unit(3) _.unit(6); + + .time { + flex: 1; + font: var(--font-label-2); + } + } + + &.dark { + // Sync with iframe's UI color + background: #1a1c1d; + + .topBar { + color: #fff; + } + } + + &.light { + // Sync with iframe's UI color + background: #fff; + + .topBar { + color: #000; + } + } + + iframe { + width: 390px; + height: 808px; + } + } + + @media screen and (min-height: 1100px) { + transform: unset; + height: 900px; + } + + @media screen and (min-height: 900px) and (max-height: 1100px) { + transform: scale(0.75); + height: 675px; + } + } + } +} diff --git a/packages/console/src/components/SignInExperiencePreview/index.tsx b/packages/console/src/components/SignInExperiencePreview/index.tsx new file mode 100644 index 000000000..4e7d81a3f --- /dev/null +++ b/packages/console/src/components/SignInExperiencePreview/index.tsx @@ -0,0 +1,127 @@ +import type { LanguageTag } from '@logto/language-kit'; +import type { ConnectorMetadata, ConnectorResponse, SignInExperience } from '@logto/schemas'; +import { ConnectorType, AppearanceMode } from '@logto/schemas'; +import { conditional } from '@silverhand/essentials'; +import classNames from 'classnames'; +import { format } from 'date-fns'; +import { useContext, useRef, useMemo, useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import useSWR from 'swr'; + +import PhoneInfo from '@/assets/images/phone-info.svg'; +import { AppEndpointsContext } from '@/contexts/AppEndpointsProvider'; +import type { RequestError } from '@/hooks/use-api'; +import useUiLanguages from '@/hooks/use-ui-languages'; + +import * as styles from './index.module.scss'; +import { PreviewPlatform } from './types'; + +type Props = { + platform: PreviewPlatform; + mode: Omit; + language: LanguageTag; + signInExperience?: SignInExperience; +}; + +const SignInExperiencePreview = ({ platform, mode, language, signInExperience }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const { customPhrases } = useUiLanguages(); + const { userEndpoint } = useContext(AppEndpointsContext); + const previewRef = useRef(null); + const { data: allConnectors } = useSWR('api/connectors'); + + const configForUiPage = useMemo(() => { + if (!allConnectors || !signInExperience) { + return; + } + + const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce< + Array + >( + (previous, connectorTarget) => [ + ...previous, + ...allConnectors.filter(({ target }) => target === connectorTarget), + ], + [] + ); + + const hasEmailConnector = allConnectors.some(({ type }) => type === ConnectorType.Email); + + const hasSmsConnector = allConnectors.some(({ type }) => type === ConnectorType.Sms); + + return { + signInExperience: { + ...signInExperience, + socialConnectors, + forgotPassword: { + email: hasEmailConnector, + sms: hasSmsConnector, + }, + }, + language, + mode, + platform: platform === PreviewPlatform.DesktopWeb ? 'web' : 'mobile', + isNative: platform === PreviewPlatform.Mobile, + }; + }, [allConnectors, language, mode, platform, signInExperience]); + + const postPreviewMessage = useCallback(() => { + if (!configForUiPage || !customPhrases) { + return; + } + + previewRef.current?.contentWindow?.postMessage( + { sender: 'ac_preview', config: configForUiPage }, + userEndpoint?.origin ?? '' + ); + }, [userEndpoint?.origin, configForUiPage, customPhrases]); + + useEffect(() => { + postPreviewMessage(); + + const iframe = previewRef.current; + + iframe?.addEventListener('load', postPreviewMessage); + + return () => { + iframe?.removeEventListener('load', postPreviewMessage); + }; + }, [postPreviewMessage]); + + return ( +
+
+
+ {platform !== PreviewPlatform.DesktopWeb && ( +
+
{format(Date.now(), 'HH:mm')}
+ +
+ )} +