From 57eb6ee452d5e9893bab6f1e2bf6f45d02e2f34f Mon Sep 17 00:00:00 2001 From: simeng-li Date: Tue, 14 Mar 2023 11:06:01 +0800 Subject: [PATCH] feat(schemas,ui): add custom content slot (#3369) --- .changeset-staged/smooth-steaks-cough.md | 9 ++++++ .../core/src/__mocks__/sign-in-experience.ts | 1 + .../src/queries/sign-in-experience.test.ts | 3 +- packages/core/src/utils/zod.test.ts | 16 ++++++++++- packages/core/src/utils/zod.ts | 7 +++++ .../next-1678450233-support-custom-content.ts | 20 +++++++++++++ .../schemas/src/foundations/jsonb-types.ts | 8 ++++++ .../schemas/src/seeds/sign-in-experience.ts | 1 + .../schemas/tables/sign_in_experiences.sql | 1 + .../ui/src/Layout/AppLayout/CustomContent.tsx | 28 +++++++++++++++++++ .../ui/src/Layout/AppLayout/index.module.scss | 15 ++++------ packages/ui/src/Layout/AppLayout/index.tsx | 14 ++++++---- .../ui/src/Layout/LandingPageLayout/index.tsx | 3 +- packages/ui/src/__mocks__/logto.tsx | 2 ++ packages/ui/src/hooks/use-sie.ts | 2 ++ packages/ui/src/utils/consts.ts | 7 +++++ 16 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 .changeset-staged/smooth-steaks-cough.md create mode 100644 packages/schemas/alterations/next-1678450233-support-custom-content.ts create mode 100644 packages/ui/src/Layout/AppLayout/CustomContent.tsx create mode 100644 packages/ui/src/utils/consts.ts diff --git a/.changeset-staged/smooth-steaks-cough.md b/.changeset-staged/smooth-steaks-cough.md new file mode 100644 index 000000000..7020e6805 --- /dev/null +++ b/.changeset-staged/smooth-steaks-cough.md @@ -0,0 +1,9 @@ +--- +"@logto/core": minor +"@logto/schemas": minor +"@logto/ui": minor +--- + +### Add custom content sign-in-experience settings to allow insert custom static html content to the logto sign-in pages + +- feat: combine with the custom css, give the user the ability to further customize the sign-in pages diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index 5134bd1ad..a4417fc38 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -90,4 +90,5 @@ export const mockSignInExperience: SignInExperience = { socialSignInConnectorTargets: ['github', 'facebook', 'wechat'], signInMode: SignInMode.SignInAndRegister, customCss: null, + customContent: {}, }; diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index 5ffc6ed03..0915fd796 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -30,12 +30,13 @@ describe('sign-in-experience query', () => { signIn: JSON.stringify(mockSignInExperience.signIn), signUp: JSON.stringify(mockSignInExperience.signUp), socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets), + customContent: JSON.stringify(mockSignInExperience.customContent), }; it('findDefaultSignInExperience', async () => { /* eslint-disable sql/no-unsafe-query */ const expectSql = ` - select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css" + select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css", "custom_content" from "sign_in_experiences" where "id"=$1 `; diff --git a/packages/core/src/utils/zod.test.ts b/packages/core/src/utils/zod.test.ts index 5b04acc0f..c8972c2c1 100644 --- a/packages/core/src/utils/zod.test.ts +++ b/packages/core/src/utils/zod.test.ts @@ -1,5 +1,10 @@ import { languages, languageTagGuard } from '@logto/language-kit'; -import { ApplicationType, arbitraryObjectGuard, translationGuard } from '@logto/schemas'; +import { + ApplicationType, + arbitraryObjectGuard, + translationGuard, + customContentGuard, +} from '@logto/schemas'; import { string, boolean, number, object, nativeEnum, unknown, literal, union } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; @@ -220,4 +225,13 @@ describe('zodTypeToSwagger', () => { new RequestError('swagger.invalid_zod_type', 'test') ); }); + + it('record type', () => { + expect(zodTypeToSwagger(customContentGuard)).toEqual({ + type: 'object', + additionalProperties: { + type: 'string', + }, + }); + }); }); diff --git a/packages/core/src/utils/zod.ts b/packages/core/src/utils/zod.ts index 82aae8c9a..e8d7f3211 100644 --- a/packages/core/src/utils/zod.ts +++ b/packages/core/src/utils/zod.ts @@ -240,6 +240,13 @@ export const zodTypeToSwagger = ( }; } + if (config instanceof ZodRecord) { + return { + type: 'object', + additionalProperties: zodTypeToSwagger(config.valueSchema), + }; + } + // TO-DO: Improve swagger output for zod schema with refinement (validate through JS functions) if (config instanceof ZodEffects && config._def.effect.type === 'refinement') { return { diff --git a/packages/schemas/alterations/next-1678450233-support-custom-content.ts b/packages/schemas/alterations/next-1678450233-support-custom-content.ts new file mode 100644 index 000000000..a3333b109 --- /dev/null +++ b/packages/schemas/alterations/next-1678450233-support-custom-content.ts @@ -0,0 +1,20 @@ +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table sign_in_experiences + add column if not exists custom_content jsonb not null default '{}'::jsonb; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table sign_in_experiences + drop column custom_content; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 06e9eb579..e3ef64668 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -143,8 +143,16 @@ export const connectorTargetsGuard = z.string().array(); export type ConnectorTargets = z.infer; +export const customContentGuard = z.record(z.string()); + +export type CustomContent = z.infer; + /* === Logto Configs === */ +/** + * Settings + */ + export enum AppearanceMode { SyncWithSystem = 'system', LightMode = 'light', diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index 800e20893..aa86cc255 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -44,6 +44,7 @@ export const createDefaultSignInExperience = (forTenantId: string): Readonly { + const { customContent } = useSieMethods(); + const { pathname } = useLocation(); + + const customHtml = customContent?.[pathname]; + + if (!customHtml) { + return null; + } + + try { + // Expected error; CustomContent content is load from Logto remote server + // eslint-disable-next-line react/no-danger + return
; + } catch { + return null; + } +}; + +export default CustomContent; diff --git a/packages/ui/src/Layout/AppLayout/index.module.scss b/packages/ui/src/Layout/AppLayout/index.module.scss index b93a108c6..dfa2b0189 100644 --- a/packages/ui/src/Layout/AppLayout/index.module.scss +++ b/packages/ui/src/Layout/AppLayout/index.module.scss @@ -24,18 +24,13 @@ .container { min-height: 100%; - @include _.flex_column(center, normal); + @include _.flex_column(center, center); } .main { @include _.flex_column; } -.placeHolder { - flex: 1; - min-height: _.unit(5); -} - :global(body.mobile) { .container { padding-bottom: env(safe-area-inset-bottom); @@ -48,13 +43,13 @@ position: relative; background: var(--color-bg-body); } - - .placeHolder { - display: none; - } } :global(body.desktop) { + .container { + padding: _.unit(5); + } + .main { width: 640px; min-height: 640px; diff --git a/packages/ui/src/Layout/AppLayout/index.tsx b/packages/ui/src/Layout/AppLayout/index.tsx index 3a1eb0058..e04e3e07e 100644 --- a/packages/ui/src/Layout/AppLayout/index.tsx +++ b/packages/ui/src/Layout/AppLayout/index.tsx @@ -1,10 +1,13 @@ +import classNames from 'classnames'; import { useEffect } from 'react'; import { Outlet, useLocation } from 'react-router-dom'; import LogtoSignature from '@/components/LogtoSignature'; import usePlatform from '@/hooks/use-platform'; +import { layoutClassNames } from '@/utils/consts'; import { parseHtmlTitle } from '@/utils/sign-in-experience'; +import CustomContent from './CustomContent'; import * as styles from './index.module.scss'; const AppLayout = () => { @@ -23,14 +26,13 @@ const AppLayout = () => { return (
-
-
-
+
+ {!isMobile && } +
- {isMobile && } + {isMobile && }
- {!isMobile && } -
+ {!isMobile && }
); diff --git a/packages/ui/src/Layout/LandingPageLayout/index.tsx b/packages/ui/src/Layout/LandingPageLayout/index.tsx index 3b54777d5..60e0a1836 100644 --- a/packages/ui/src/Layout/LandingPageLayout/index.tsx +++ b/packages/ui/src/Layout/LandingPageLayout/index.tsx @@ -5,6 +5,7 @@ import type { TFuncKey } from 'react-i18next'; import BrandingHeader from '@/components/BrandingHeader'; import { PageContext } from '@/hooks/use-page-context'; +import { layoutClassNames } from '@/utils/consts'; import { getBrandingLogoUrl } from '@/utils/logo'; import AppNotification from '../../containers/AppNotification'; @@ -33,7 +34,7 @@ const LandingPageLayout = ({ children, className, title }: Props) => { {platform === 'web' &&
}
diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index dcbd992cf..c9bdea516 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -203,6 +203,7 @@ export const mockSignInExperience: SignInExperience = { socialSignInConnectorTargets: ['BE8QXN0VsrOH7xdWFDJZ9', 'lcXT4o2GSjbV9kg2shZC7'], signInMode: SignInMode.SignInAndRegister, customCss: null, + customContent: {}, }; export const mockSignInExperienceSettings: SignInExperienceResponse = { @@ -226,6 +227,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = { phone: true, }, customCss: null, + customContent: {}, }; const usernameSettings = { diff --git a/packages/ui/src/hooks/use-sie.ts b/packages/ui/src/hooks/use-sie.ts index 4bddefd21..56df2b9ab 100644 --- a/packages/ui/src/hooks/use-sie.ts +++ b/packages/ui/src/hooks/use-sie.ts @@ -20,6 +20,8 @@ export const useSieMethods = () => { socialConnectors: experienceSettings?.socialConnectors ?? [], signInMode: experienceSettings?.signInMode, forgotPassword: experienceSettings?.forgotPassword, + customCss: experienceSettings?.customCss, + customContent: experienceSettings?.customContent, }; }; diff --git a/packages/ui/src/utils/consts.ts b/packages/ui/src/utils/consts.ts new file mode 100644 index 000000000..00eeb7c0e --- /dev/null +++ b/packages/ui/src/utils/consts.ts @@ -0,0 +1,7 @@ +export const layoutClassNames = Object.freeze({ + pageContainer: 'logto_page-container', + mainContent: 'logto_main-content', + customContent: 'logto_custom-content', + signature: 'logto_signature', + brandingHeader: 'logto_branding-header', +});