From 1ef5519e7550b4a5067e15100e9d3a0a944b7d22 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 21 Feb 2023 10:55:44 +0800 Subject: [PATCH] feat: custom css (#3155) --- .../core/src/__mocks__/sign-in-experience.ts | 1 + .../src/queries/sign-in-experience.test.ts | 2 +- .../next-1676874936-support-custom-css.ts | 20 +++++ .../schemas/src/seeds/sign-in-experience.ts | 82 +++++++++---------- .../schemas/tables/sign_in_experiences.sql | 1 + packages/ui/src/App.tsx | 9 +- packages/ui/src/__mocks__/logto.tsx | 2 + 7 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 packages/schemas/alterations/next-1676874936-support-custom-css.ts diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index 9a7d87232..2a4ce1b1a 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -91,4 +91,5 @@ export const mockSignInExperience: SignInExperience = { }, socialSignInConnectorTargets: ['github', 'facebook', 'wechat'], signInMode: SignInMode.SignInAndRegister, + customCss: null, }; diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index 51e5048ac..0fde93b2d 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -35,7 +35,7 @@ describe('sign-in-experience query', () => { it('findDefaultSignInExperience', async () => { /* eslint-disable sql/no-unsafe-query */ const expectSql = ` - select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode" + select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css" from "sign_in_experiences" where "id"=$1 `; diff --git a/packages/schemas/alterations/next-1676874936-support-custom-css.ts b/packages/schemas/alterations/next-1676874936-support-custom-css.ts new file mode 100644 index 000000000..6c25619a2 --- /dev/null +++ b/packages/schemas/alterations/next-1676874936-support-custom-css.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_css text; + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table sign_in_experiences + drop column custom_css; + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index b5f014a04..96bd6f0b3 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -1,55 +1,55 @@ import { generateDarkColor } from '@logto/core-kit'; -import type { CreateSignInExperience } from '../db-entries/index.js'; +import type { SignInExperience } from '../db-entries/index.js'; import { SignInMode } from '../db-entries/index.js'; import { BrandingStyle, SignInIdentifier } from '../foundations/index.js'; import { defaultTenantId } from './tenant.js'; const defaultPrimaryColor = '#6139F6'; -export const createDefaultSignInExperience = ( - forTenantId: string -): Readonly => ({ - tenantId: forTenantId, - id: 'default', - color: { - primaryColor: defaultPrimaryColor, - isDarkModeEnabled: false, - darkPrimaryColor: generateDarkColor(defaultPrimaryColor), - }, - branding: { - style: BrandingStyle.Logo, - logoUrl: 'https://logto.io/logo.svg', - darkLogoUrl: 'https://logto.io/logo-dark.svg', - }, - languageInfo: { - autoDetect: true, - fallbackLanguage: 'en', - }, - termsOfUseUrl: null, - signUp: { - identifiers: [SignInIdentifier.Username], - password: true, - verify: false, - }, - signIn: { - methods: [ - { - identifier: SignInIdentifier.Username, - password: true, - verificationCode: false, - isPasswordPrimary: true, - }, - ], - }, - socialSignInConnectorTargets: [], - signInMode: SignInMode.SignInAndRegister, -}); +export const createDefaultSignInExperience = (forTenantId: string): Readonly => + Object.freeze({ + tenantId: forTenantId, + id: 'default', + color: { + primaryColor: defaultPrimaryColor, + isDarkModeEnabled: false, + darkPrimaryColor: generateDarkColor(defaultPrimaryColor), + }, + branding: { + style: BrandingStyle.Logo, + logoUrl: 'https://logto.io/logo.svg', + darkLogoUrl: 'https://logto.io/logo-dark.svg', + }, + languageInfo: { + autoDetect: true, + fallbackLanguage: 'en' as const, + }, + termsOfUseUrl: null, + signUp: { + identifiers: [SignInIdentifier.Username], + password: true, + verify: false, + }, + signIn: { + methods: [ + { + identifier: SignInIdentifier.Username, + password: true, + verificationCode: false, + isPasswordPrimary: true, + }, + ], + }, + socialSignInConnectorTargets: [], + signInMode: SignInMode.SignInAndRegister, + customCss: null, + }); /** @deprecated Use `createDefaultSignInExperience()` instead. */ export const defaultSignInExperience = createDefaultSignInExperience(defaultTenantId); -export const adminConsoleSignInExperience: CreateSignInExperience = { +export const adminConsoleSignInExperience: Readonly = Object.freeze({ ...defaultSignInExperience, color: { ...defaultSignInExperience.color, @@ -61,4 +61,4 @@ export const adminConsoleSignInExperience: CreateSignInExperience = { darkLogoUrl: 'https://logto.io/logo-dark.svg', slogan: 'admin_console.welcome.title', // TODO: @simeng should we programmatically support an i18n key for slogan? }, -}; +}); diff --git a/packages/schemas/tables/sign_in_experiences.sql b/packages/schemas/tables/sign_in_experiences.sql index c785ae008..34a4e3878 100644 --- a/packages/schemas/tables/sign_in_experiences.sql +++ b/packages/schemas/tables/sign_in_experiences.sql @@ -12,5 +12,6 @@ create table sign_in_experiences ( sign_up jsonb /* @use SignUp */ not null, social_sign_in_connector_targets jsonb /* @use ConnectorTargets */ not null default '[]'::jsonb, sign_in_mode sign_in_mode not null default 'SignInAndRegister', + custom_css text, primary key (tenant_id, id) ); diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index b7fd0b645..c984b7ddf 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,5 +1,5 @@ import { SignInMode } from '@logto/schemas'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom'; import AppBoundary from './containers/AppBoundary'; @@ -29,8 +29,13 @@ import './scss/normalized.scss'; const App = () => { const { context, Provider } = usePageContext(); const { experienceSettings, setLoading, setExperienceSettings } = context; + const customCssRef = useRef(document.createElement('style')); const [isPreview] = usePreview(context); + useEffect(() => { + document.head.append(customCssRef.current); + }, []); + useEffect(() => { if (isPreview) { return; @@ -38,6 +43,8 @@ const App = () => { (async () => { const settings = await getSignInExperienceSettings(); + // eslint-disable-next-line @silverhand/fp/no-mutation + customCssRef.current.textContent = settings.customCss; // Note: i18n must be initialized ahead of page render await initI18n(settings.languageInfo); diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index 5d2d1e382..885e34394 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -209,6 +209,7 @@ export const mockSignInExperience: SignInExperience = { }, socialSignInConnectorTargets: ['BE8QXN0VsrOH7xdWFDJZ9', 'lcXT4o2GSjbV9kg2shZC7'], signInMode: SignInMode.SignInAndRegister, + customCss: null, }; export const mockSignInExperienceSettings: SignInExperienceResponse = { @@ -230,6 +231,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = { email: true, phone: true, }, + customCss: null, }; const usernameSettings = {