diff --git a/.changeset-staged/nice-lions-dream.md b/.changeset-staged/nice-lions-dream.md new file mode 100644 index 000000000..fef680128 --- /dev/null +++ b/.changeset-staged/nice-lions-dream.md @@ -0,0 +1,9 @@ +--- +"@logto/console": minor +"@logto/core": minor +"@logto/phrases": minor +"@logto/schemas": minor +"@logto/ui": minor +--- + +remove the branding style config and make the logo URL config optional diff --git a/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx b/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx index 8d2eb33d9..e6c134554 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Branding/BrandingForm.tsx @@ -1,10 +1,8 @@ -import { BrandingStyle } from '@logto/schemas'; -import { Controller, useFormContext } from 'react-hook-form'; +import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import Card from '@/components/Card'; import FormField from '@/components/FormField'; -import RadioGroup, { Radio } from '@/components/RadioGroup'; import TextInput from '@/components/TextInput'; import { uriValidator } from '@/utils/validator'; @@ -16,17 +14,15 @@ const BrandingForm = () => { const { watch, register, - control, formState: { errors }, } = useFormContext(); const isDarkModeEnabled = watch('color.isDarkModeEnabled'); - const style = watch('branding.style'); - const isSloganRequired = style === BrandingStyle.Logo_Slogan; return (
{t('sign_in_exp.branding.title')}
+ { placeholder={t('sign_in_exp.branding.favicon')} /> - - ( - - - - - )} - /> - - + !value || uriValidator(value) || t('errors.invalid_uri_format'), })} hasError={Boolean(errors.branding?.logoUrl)} @@ -76,15 +55,6 @@ const BrandingForm = () => { /> )} - {isSloganRequired && ( - - - - )}
); }; diff --git a/packages/console/src/pages/SignInExperience/utils/form.ts b/packages/console/src/pages/SignInExperience/utils/form.ts index 726e635d6..fea388a09 100644 --- a/packages/console/src/pages/SignInExperience/utils/form.ts +++ b/packages/console/src/pages/SignInExperience/utils/form.ts @@ -48,8 +48,8 @@ export const signInExperienceParser = { ...branding, // Transform empty string to undefined favicon: conditional(branding.favicon?.length && branding.favicon), + logoUrl: conditional(branding.logoUrl?.length && branding.logoUrl), darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl), - slogan: conditional(branding.slogan?.length && branding.slogan), }, signUp: signUp ? signInExperienceParser.toRemoteSignUp(signUp) diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index 1373e93a8..5134bd1ad 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -6,7 +6,7 @@ import type { SignUp, SignIn, } from '@logto/schemas'; -import { BrandingStyle, SignInMode, SignInIdentifier } from '@logto/schemas'; +import { SignInMode, SignInIdentifier } from '@logto/schemas'; export const mockColor: Color = { primaryColor: '#000', @@ -15,9 +15,7 @@ export const mockColor: Color = { }; export const mockBranding: Branding = { - style: BrandingStyle.Logo_Slogan, logoUrl: 'http://silverhand.png', - slogan: 'Silverhand.', }; export const mockTermsOfUseUrl = 'http://silverhand.com/terms'; @@ -54,9 +52,7 @@ export const mockSignInExperience: SignInExperience = { darkPrimaryColor: '#fff', }, branding: { - style: BrandingStyle.Logo, logoUrl: 'http://logto.png', - slogan: 'logto', }, termsOfUseUrl: mockTermsOfUseUrl, privacyPolicyUrl: mockPrivacyPolicyUrl, diff --git a/packages/core/src/libraries/sign-in-experience/index.test.ts b/packages/core/src/libraries/sign-in-experience/index.test.ts index 8aaa39be9..b48305b2d 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -1,12 +1,10 @@ import type { LanguageTag } from '@logto/language-kit'; import { builtInLanguages } from '@logto/phrases-ui'; import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; -import { BrandingStyle } from '@logto/schemas'; import { socialTarget01, socialTarget02, - mockBranding, mockSignInExperience, mockSocialConnectors, } from '#src/__mocks__/index.js'; @@ -42,7 +40,7 @@ const queries = new MockQueries({ const connectorLibrary = createConnectorLibrary(queries); const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors'); -const { validateBranding, createSignInExperienceLibrary } = await import('./index.js'); +const { createSignInExperienceLibrary } = await import('./index.js'); const { validateLanguageInfo, removeUnavailableSocialConnectorTargets } = createSignInExperienceLibrary(queries, connectorLibrary); @@ -50,48 +48,6 @@ beforeEach(() => { jest.clearAllMocks(); }); -describe('validate branding', () => { - it('should throw when the UI style contains the slogan and slogan is empty', () => { - expect(() => { - validateBranding({ - ...mockBranding, - style: BrandingStyle.Logo_Slogan, - slogan: '', - }); - }).toMatchError(new RequestError('sign_in_experiences.empty_slogan')); - }); - - it('should throw when the logo is empty', () => { - expect(() => { - validateBranding({ - ...mockBranding, - style: BrandingStyle.Logo, - logoUrl: ' ', - slogan: '', - }); - }).toMatchError(new RequestError('sign_in_experiences.empty_logo')); - }); - - it('should throw when the UI style contains the slogan and slogan is blank', () => { - expect(() => { - validateBranding({ - ...mockBranding, - style: BrandingStyle.Logo_Slogan, - slogan: ' \t\n', - }); - }).toMatchError(new RequestError('sign_in_experiences.empty_slogan')); - }); - - it('should not throw when the UI style does not contain the slogan and slogan is empty', () => { - expect(() => { - validateBranding({ - ...mockBranding, - style: BrandingStyle.Logo, - }); - }).not.toThrow(); - }); -}); - describe('validate language info', () => { it('should call findAllCustomLanguageTags', async () => { await validateLanguageInfo({ diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 2bc534464..5d055b63f 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -1,8 +1,7 @@ import { builtInLanguages } from '@logto/phrases-ui'; -import type { Branding, LanguageInfo, SignInExperience } from '@logto/schemas'; -import { ConnectorType, BrandingStyle } from '@logto/schemas'; +import type { LanguageInfo, SignInExperience } from '@logto/schemas'; +import { ConnectorType } from '@logto/schemas'; import { deduplicate } from '@silverhand/essentials'; -import i18next from 'i18next'; import RequestError from '#src/errors/RequestError/index.js'; import type { ConnectorLibrary } from '#src/libraries/connector.js'; @@ -12,14 +11,6 @@ import assertThat from '#src/utils/assert-that.js'; export * from './sign-up.js'; export * from './sign-in.js'; -export const validateBranding = (branding: Branding) => { - if (branding.style === BrandingStyle.Logo_Slogan) { - assertThat(branding.slogan?.trim(), 'sign_in_experiences.empty_slogan'); - } - - assertThat(branding.logoUrl.trim(), 'sign_in_experiences.empty_logo'); -}; - export type SignInExperienceLibrary = ReturnType; export const createSignInExperienceLibrary = ( @@ -61,16 +52,7 @@ export const createSignInExperienceLibrary = ( }); }; - const getSignInExperience = async (): Promise => { - const raw = await findDefaultSignInExperience(); - const { branding } = raw; - - // Alter sign-in experience dynamic configs - return Object.freeze({ - ...raw, - branding: { ...branding, slogan: branding.slogan && i18next.t(branding.slogan) }, - }); - }; + const getSignInExperience = async (): Promise => findDefaultSignInExperience(); return { validateLanguageInfo, diff --git a/packages/core/src/routes/sign-in-experience/guard.branding.test.ts b/packages/core/src/routes/sign-in-experience/guard.branding.test.ts index 5eab88136..05ce6406a 100644 --- a/packages/core/src/routes/sign-in-experience/guard.branding.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.branding.test.ts @@ -1,5 +1,4 @@ import type { CreateSignInExperience, SignInExperience } from '@logto/schemas'; -import { BrandingStyle } from '@logto/schemas'; import { pickDefault } from '@logto/shared/esm'; import { mockBranding, mockSignInExperience } from '#src/__mocks__/index.js'; @@ -31,18 +30,6 @@ const expectPatchResponseStatus = async ( }; describe('branding', () => { - describe('style', () => { - test.each(Object.values(BrandingStyle))('%p should succeed', async (style) => { - const signInExperience = { branding: { ...mockBranding, style } }; - await expectPatchResponseStatus(signInExperience, 200); - }); - - test.each([undefined, '', 'invalid'])('%p should fail', async (style) => { - const signInExperience = { branding: { ...mockBranding, style } }; - await expectPatchResponseStatus(signInExperience, 400); - }); - }); - describe('logoUrl', () => { test.each(['http://silverhand.com/silverhand.png', 'https://logto.dev/logto.jpg'])( '%p should success', @@ -52,36 +39,14 @@ describe('branding', () => { } ); - test.each([undefined, null, '', 'invalid'])('%p should fail', async (logoUrl) => { + test.each([null, '', 'invalid'])('%p should fail', async (logoUrl) => { const signInExperience = { branding: { ...mockBranding, logoUrl } }; await expectPatchResponseStatus(signInExperience, 400); }); - }); - describe('slogan', () => { - test.each([undefined, 'Silverhand.', 'Supercharge innovations.'])( - '%p should success', - async (slogan) => { - const signInExperience = { - branding: { - ...mockBranding, - style: BrandingStyle.Logo, - slogan, - }, - }; - await expectPatchResponseStatus(signInExperience, 200); - } - ); - - test.each([null])('%p should fail', async (slogan) => { - const signInExperience = { - branding: { - ...mockBranding, - style: BrandingStyle.Logo, - slogan, - }, - }; - await expectPatchResponseStatus(signInExperience, 400); + it('should success when logoUrl is not provided', async () => { + const signInExperience = { branding: { ...mockBranding, logoUrl: undefined } }; + await expectPatchResponseStatus(signInExperience, 200); }); }); diff --git a/packages/core/src/routes/sign-in-experience/index.test.ts b/packages/core/src/routes/sign-in-experience/index.test.ts index 46711da2a..1920ec191 100644 --- a/packages/core/src/routes/sign-in-experience/index.test.ts +++ b/packages/core/src/routes/sign-in-experience/index.test.ts @@ -30,10 +30,9 @@ const logtoConnectors = [ mockAliyunSmsConnector, ]; -const { validateBranding, validateSignIn, validateSignUp } = await mockEsmWithActual( +const { validateSignIn, validateSignUp } = await mockEsmWithActual( '#src/libraries/sign-in-experience/index.js', () => ({ - validateBranding: jest.fn(), validateSignIn: jest.fn(), validateSignUp: jest.fn(), }) @@ -125,7 +124,6 @@ describe('PATCH /sign-in-exp', () => { signIn: mockSignIn, }); - expect(validateBranding).toHaveBeenCalledWith(mockBranding); expect(validateLanguageInfo).toHaveBeenCalledWith(mockLanguageInfo); expect(validateSignUp).toHaveBeenCalledWith(mockSignUp, logtoConnectors); expect(validateSignIn).toHaveBeenCalledWith(mockSignIn, mockSignUp, logtoConnectors); diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts index 45a6e0e02..9c502e6e0 100644 --- a/packages/core/src/routes/sign-in-experience/index.ts +++ b/packages/core/src/routes/sign-in-experience/index.ts @@ -1,11 +1,7 @@ import { ConnectorType, SignInExperiences } from '@logto/schemas'; import { literal, object, string } from 'zod'; -import { - validateBranding, - validateSignUp, - validateSignIn, -} from '#src/libraries/sign-in-experience/index.js'; +import { validateSignUp, validateSignIn } from '#src/libraries/sign-in-experience/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import type { AuthedRouter, RouterInitArgs } from '../types.js'; @@ -44,11 +40,7 @@ export default function signInExperiencesRoutes( }), async (ctx, next) => { const { socialSignInConnectorTargets, ...rest } = ctx.guard.body; - const { branding, languageInfo, signUp, signIn } = rest; - - if (branding) { - validateBranding(branding); - } + const { languageInfo, signUp, signIn } = rest; if (languageInfo) { await validateLanguageInfo(languageInfo); diff --git a/packages/integration-tests/src/tests/api/sign-in-experience.test.ts b/packages/integration-tests/src/tests/api/sign-in-experience.test.ts index fc5127b9c..7a55f4bf8 100644 --- a/packages/integration-tests/src/tests/api/sign-in-experience.test.ts +++ b/packages/integration-tests/src/tests/api/sign-in-experience.test.ts @@ -1,5 +1,3 @@ -import { BrandingStyle } from '@logto/schemas'; - import { getSignInExperience, updateSignInExperience } from '#src/api/index.js'; describe('admin console sign-in experience', () => { @@ -17,8 +15,6 @@ describe('admin console sign-in experience', () => { isDarkModeEnabled: true, }, branding: { - style: BrandingStyle.Logo_Slogan, - slogan: 'Logto Slogan', logoUrl: 'https://logto.io/new-logo.png', darkLogoUrl: 'https://logto.io/new-dark-logo.png', }, diff --git a/packages/phrases/src/locales/de/errors.ts b/packages/phrases/src/locales/de/errors.ts index 5f4b67fb5..e56fb5433 100644 --- a/packages/phrases/src/locales/de/errors.ts +++ b/packages/phrases/src/locales/de/errors.ts @@ -144,9 +144,6 @@ const errors = { sign_in_experiences: { empty_content_url_of_terms_of_use: 'Leere "Nutzungsbedingungen" URL. Bitte füge die URL hinzu, wenn "Nutzungsbedingungen" aktiviert ist.', - empty_logo: 'Bitte füge eine Logo URL hinzu.', - empty_slogan: - 'Leerer Branding-Slogan. Bitte füge einen Branding-Slogan hinzu, wenn ein UI-Stil ausgewählt wird, der den Slogan enthält.', empty_social_connectors: 'Leere Social Connectors. Bitte füge aktivierte Social Connectoren hinzu, wenn Social Anmeldung aktiviert ist.', enabled_connector_not_found: 'Aktivierter {{type}} Connector nicht gefunden.', diff --git a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts index c2a185485..3ab21fa52 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts @@ -85,16 +85,10 @@ const sign_in_exp = { title: 'BRANDING', ui_style: 'Stil', favicon: 'Browser favicon', // UNTRANSLATED - styles: { - logo_slogan: 'App logo mit Slogan', - logo: 'Nur App logo', - }, logo_image_url: 'App logo URL', logo_image_url_placeholder: 'https://dein.cdn.domain/logo.png', dark_logo_image_url: 'App logo URL (Dunkler Modus)', dark_logo_image_url_placeholder: 'https://dein.cdn.domain/logo-dark.png', - slogan: 'Slogan', - slogan_placeholder: 'Entfessle deine Kreativität', }, others: { terms_of_use: { diff --git a/packages/phrases/src/locales/en/errors.ts b/packages/phrases/src/locales/en/errors.ts index eb3f85e6b..f54c5f2ea 100644 --- a/packages/phrases/src/locales/en/errors.ts +++ b/packages/phrases/src/locales/en/errors.ts @@ -143,9 +143,6 @@ const errors = { sign_in_experiences: { empty_content_url_of_terms_of_use: 'Empty "Terms of use" content URL. Please add the content URL if "Terms of use" is enabled.', - empty_logo: 'Please enter your logo URL', - empty_slogan: - 'Empty branding slogan. Please add a branding slogan if a UI style containing the slogan is selected.', empty_social_connectors: 'Empty social connectors. Please add enabled social connectors when the social sign-in method is enabled.', enabled_connector_not_found: 'Enabled {{type}} connector not found.', diff --git a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts index e636a07ca..7eb8570e9 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts @@ -28,16 +28,10 @@ const sign_in_exp = { title: 'BRANDING AREA', ui_style: 'Style', favicon: 'Browser favicon', - styles: { - logo_slogan: 'App logo with slogan', - logo: 'App logo only', - }, logo_image_url: 'App logo image URL', logo_image_url_placeholder: 'https://your.cdn.domain/logo.png', dark_logo_image_url: 'App logo image URL (Dark)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', - slogan: 'Slogan', - slogan_placeholder: 'Unleash your creativity', }, sign_up_and_sign_in: { identifiers_email: 'Email address', diff --git a/packages/phrases/src/locales/fr/errors.ts b/packages/phrases/src/locales/fr/errors.ts index 8569fdb6c..e27da7570 100644 --- a/packages/phrases/src/locales/fr/errors.ts +++ b/packages/phrases/src/locales/fr/errors.ts @@ -150,9 +150,6 @@ const errors = { sign_in_experiences: { empty_content_url_of_terms_of_use: 'URL de contenu "Conditions d\'utilisation" vide. Veuillez ajouter l\'URL du contenu si les "Conditions d\'utilisation" sont activées.', - empty_logo: "Veuillez entrer l'URL de votre logo", - empty_slogan: - "Un slogan vide. Veuillez ajouter un slogan si un style d'interface utilisateur contenant le slogan est sélectionné.", empty_social_connectors: 'Connecteurs sociaux vides. Veuillez ajouter des connecteurs sociaux activés lorsque la méthode de connexion sociale est activée.', enabled_connector_not_found: 'Le connecteur {{type}} activé est introuvable.', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts index f04984be2..f2c6c7b15 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts @@ -30,16 +30,10 @@ const sign_in_exp = { title: 'ZONE DE MARQUE', ui_style: 'Style', favicon: 'Browser favicon', // UNTRANSLATED - styles: { - logo_slogan: "Logo de l'application avec slogan", - logo: "Logo de l'application seulement", - }, logo_image_url: "URL de l'image du logo de l'application", logo_image_url_placeholder: 'https://votre.domaine.cdn/logo.png', dark_logo_image_url: "URL de l'image du logo de l'application (Sombre)", dark_logo_image_url_placeholder: 'https://votre.domaine.cdn/logo-dark.png', - slogan: 'Slogan', - slogan_placeholder: 'Libérez votre créativité', }, sign_up_and_sign_in: { identifiers_email: 'Email address', // UNTRANSLATED diff --git a/packages/phrases/src/locales/ko/errors.ts b/packages/phrases/src/locales/ko/errors.ts index 9f5906138..b4176d484 100644 --- a/packages/phrases/src/locales/ko/errors.ts +++ b/packages/phrases/src/locales/ko/errors.ts @@ -137,8 +137,6 @@ const errors = { sign_in_experiences: { empty_content_url_of_terms_of_use: '이용 약관 URL이 비어 있어요. 이용 약관이 활성화되어 있다면, 이용 약관 URL를 설정해 주세요.', - empty_logo: '로고 URL을 입력해 주세요.', - empty_slogan: '브랜딩 슬로건이 비어 있어요. 슬로건을 사용한다면, 내용을 설정해 주세요.', empty_social_connectors: '연동된 소셜이 없어요. 소셜 로그인을 사용한다면, 연동해 주세요.', enabled_connector_not_found: '활성된 {{type}} 연동을 찾을 수 없어요.', not_one_and_only_one_primary_sign_in_method: diff --git a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts index 636340254..5f22760ad 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts @@ -26,16 +26,10 @@ const sign_in_exp = { title: '브랜딩 영역', ui_style: '스타일', favicon: 'Browser favicon', // UNTRANSLATED - styles: { - logo_slogan: '앱 로고 & 슬로건', - logo: '앱 로고만', - }, logo_image_url: '앱 로고 이미지 URL', logo_image_url_placeholder: 'https://your.cdn.domain/logo.png', dark_logo_image_url: '앱 로고 이미지 URL (다크 모드)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', - slogan: '슬로건', - slogan_placeholder: '상상력을 펼쳐 보세요', }, sign_up_and_sign_in: { identifiers_email: '이메일 주소', diff --git a/packages/phrases/src/locales/pt-br/errors.ts b/packages/phrases/src/locales/pt-br/errors.ts index 5f4d780f5..b8e0e241a 100644 --- a/packages/phrases/src/locales/pt-br/errors.ts +++ b/packages/phrases/src/locales/pt-br/errors.ts @@ -147,9 +147,6 @@ const errors = { sign_in_experiences: { empty_content_url_of_terms_of_use: 'URL de conteúdo "Termos de uso" vazia. Adicione o URL do conteúdo se "Termos de uso" estiver ativado.', - empty_logo: 'Insira o URL do seu logotipo', - empty_slogan: - 'Slogan de marca vazio. Adicione um slogan de marca se um estilo de IU contendo o slogan for selecionado.', empty_social_connectors: 'Conectores sociais vazios. Adicione conectores sociais ativados quando o método de login social estiver ativado.', enabled_connector_not_found: 'Conector {{type}} ativado não encontrado.', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts index 5e7acd14a..9b7344033 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts @@ -29,16 +29,10 @@ const sign_in_exp = { title: 'ÁREA DE MARCA', ui_style: 'Estilo', favicon: 'Browser favicon', // UNTRANSLATED - styles: { - logo_slogan: 'Logo do aplicativo com slogan', - logo: 'Somente logotipo do aplicativo', - }, logo_image_url: 'URL da imagem do logotipo do aplicativo', logo_image_url_placeholder: 'https://your.cdn.domain/logo.png', dark_logo_image_url: 'URL da imagem do logotipo do aplicativo (Escuro)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', - slogan: 'Slogan', - slogan_placeholder: 'Use sua criatividade', }, sign_up_and_sign_in: { identifiers_email: 'Endereço de e-mail', diff --git a/packages/phrases/src/locales/pt-pt/errors.ts b/packages/phrases/src/locales/pt-pt/errors.ts index fd43f67c1..80926b2c7 100644 --- a/packages/phrases/src/locales/pt-pt/errors.ts +++ b/packages/phrases/src/locales/pt-pt/errors.ts @@ -145,9 +145,6 @@ const errors = { sign_in_experiences: { empty_content_url_of_terms_of_use: 'URL dos "Termos de uso" vazio. Adicione o URL se os "Termos de uso" estiverem ativados.', - empty_logo: 'Insira o URL do seu logotipo', - empty_slogan: - 'Slogan de marca vazio. Adicione um slogan se o estilo da interface com o slogan for selecionado.', empty_social_connectors: 'Conectores sociais vazios. Adicione conectores sociais e ative os quando o método de login social estiver ativado.', enabled_connector_not_found: 'Conector {{type}} ativado não encontrado.', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts index 82c456f32..176045ef7 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts @@ -28,16 +28,10 @@ const sign_in_exp = { title: 'ÁREA DE MARCA', ui_style: 'Estilo', favicon: 'Browser favicon', // UNTRANSLATED - styles: { - logo_slogan: 'Logo da app com slogan', - logo: 'Apenas o logo da app', - }, logo_image_url: 'URL do logotipo da app', logo_image_url_placeholder: 'https://your.cdn.domain/logo.png', dark_logo_image_url: 'URL do logotipo da app (tema escuro)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', - slogan: 'Slogan', - slogan_placeholder: 'Liberte a sua criatividade', }, sign_up_and_sign_in: { identifiers_email: 'Email address', // UNTRANSLATED diff --git a/packages/phrases/src/locales/tr-tr/errors.ts b/packages/phrases/src/locales/tr-tr/errors.ts index a4d9b3bfe..b6546f8db 100644 --- a/packages/phrases/src/locales/tr-tr/errors.ts +++ b/packages/phrases/src/locales/tr-tr/errors.ts @@ -144,9 +144,6 @@ const errors = { sign_in_experiences: { empty_content_url_of_terms_of_use: '"Kullanım Koşulları" İçerik URLi yok. Lütfen "Kullanım Koşulları" etkinse içerik URLi ekleyiniz.', - empty_logo: 'Lütfen logo URLini giriniz', - empty_slogan: - 'Marka sloganı yok. Eğer UI stili slogan içeriyorsa, lütfen bir marka sloganı ekleyin.', empty_social_connectors: 'Social connectors yok. Sosyal oturum açma yöntemi etkinleştirildiğinde lütfen etkin social connectorları ekleyiniz.', enabled_connector_not_found: 'Etkin {{type}} bağlayıcı bulunamadı.', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts index 923b80926..49df61aab 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts @@ -29,16 +29,10 @@ const sign_in_exp = { title: 'MARKA ALANI', ui_style: 'Stil', favicon: 'Browser favicon', // UNTRANSLATED - styles: { - logo_slogan: 'Sloganlı şekilde uygulama logosu', - logo: 'Yalnızca uygulama logosu', - }, logo_image_url: 'Uygulama logosu resim URLi', logo_image_url_placeholder: 'https://your.cdn.domain/logo.png', dark_logo_image_url: 'Uygulama logosu resim URLi (Koyu)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', - slogan: 'Slogan', - slogan_placeholder: 'Yaratıcılığınızı açığa çıkarın', }, sign_up_and_sign_in: { identifiers_email: 'Email address', // UNTRANSLATED diff --git a/packages/phrases/src/locales/zh-cn/errors.ts b/packages/phrases/src/locales/zh-cn/errors.ts index 31fdacaa9..c8e5bc7c8 100644 --- a/packages/phrases/src/locales/zh-cn/errors.ts +++ b/packages/phrases/src/locales/zh-cn/errors.ts @@ -130,8 +130,6 @@ const errors = { }, sign_in_experiences: { empty_content_url_of_terms_of_use: '你启用了“使用条款”,请添加使用条款 URL。', - empty_logo: '请输入 logo URL', - empty_slogan: '你选择了 App logo + 标语的布局。请输入你的标语。', empty_social_connectors: '你启用了社交登录的方式。请至少选择一个社交连接器。', enabled_connector_not_found: '未找到已启用的 {{type}} 连接器', not_one_and_only_one_primary_sign_in_method: '主要的登录方式必须有且仅有一个,请检查你的输入。', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts index 07322bd80..e05399487 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts @@ -27,16 +27,10 @@ const sign_in_exp = { title: '品牌定制区', ui_style: '样式', favicon: '浏览器地址栏图标', - styles: { - logo_slogan: 'Logo 和标语', - logo: '仅有 Logo', - }, logo_image_url: 'Logo 图片 URL', logo_image_url_placeholder: 'https://your.cdn.domain/logo.png', dark_logo_image_url: 'Logo 图片 URL (深色)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', - slogan: '标语', - slogan_placeholder: '释放你的创意', }, sign_up_and_sign_in: { identifiers_email: '邮件地址', diff --git a/packages/schemas/alterations/next-1678259693-remove-branding-style-config.ts b/packages/schemas/alterations/next-1678259693-remove-branding-style-config.ts new file mode 100644 index 000000000..c945f8e0b --- /dev/null +++ b/packages/schemas/alterations/next-1678259693-remove-branding-style-config.ts @@ -0,0 +1,112 @@ +import type { DatabaseTransactionConnection } from 'slonik'; +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +enum DeprecatedBrandingStyle { + Logo = 'Logo', + Logo_Slogan = 'Logo_Slogan', +} + +const deprecatedDefaultBrandingStyle = DeprecatedBrandingStyle.Logo_Slogan; +const deprecatedDefaultSlogan = 'admin_console.welcome.title'; + +type DeprecatedBranding = { + style?: DeprecatedBrandingStyle; + logoUrl?: string; + darkLogoUrl?: string; + favicon?: string; + slogan?: string; +}; + +type DeprecatedSignInExperience = { + id: string; + tenantId: string; + branding: DeprecatedBranding; +}; + +type Branding = { + logoUrl?: string; + darkLogoUrl?: string; + favicon?: string; +}; + +type SignInExperience = { + id: string; + tenantId: string; + branding: Branding; +}; + +const alterBranding = async ( + signInExperience: DeprecatedSignInExperience, + pool: DatabaseTransactionConnection +) => { + const { id, tenantId, branding: originBranding } = signInExperience; + + const { + style, // Extract to remove from branding + slogan, // Extract to remove from branding + logoUrl, + darkLogoUrl, + favicon, + } = originBranding; + + const branding: Branding = { logoUrl, darkLogoUrl, favicon }; + + await pool.query( + sql`update sign_in_experiences set branding = ${JSON.stringify( + branding + )} where id = ${id} and tenant_id = ${tenantId}` + ); +}; + +const rollbackBranding = async ( + signInExperience: SignInExperience, + pool: DatabaseTransactionConnection +) => { + const { + id, + tenantId, + branding: { logoUrl, darkLogoUrl, favicon }, + } = signInExperience; + + const adminBranding: DeprecatedBranding = { + style: DeprecatedBrandingStyle.Logo_Slogan, + slogan: 'admin_console.welcome.title', + logoUrl, + darkLogoUrl, + favicon, + }; + + const defaultBranding: DeprecatedBranding = { + style: DeprecatedBrandingStyle.Logo, + logoUrl, + darkLogoUrl, + favicon, + }; + + const branding: DeprecatedBranding = tenantId === 'admin' ? adminBranding : defaultBranding; + + await pool.query( + sql`update sign_in_experiences set branding = ${JSON.stringify( + branding + )} where id = ${id} and tenant_id = ${tenantId}` + ); +}; + +const alteration: AlterationScript = { + up: async (pool) => { + const rows = await pool.many( + sql`select * from sign_in_experiences` + ); + + await Promise.all(rows.map(async (row) => alterBranding(row, pool))); + }, + down: async (pool) => { + const rows = await pool.many(sql`select * from sign_in_experiences`); + + await Promise.all(rows.map(async (row) => rollbackBranding(row, pool))); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 57e46007d..7c0798cf8 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -103,16 +103,9 @@ export const colorGuard = z.object({ export type Color = z.infer; -export enum BrandingStyle { - Logo = 'Logo', - Logo_Slogan = 'Logo_Slogan', -} - export const brandingGuard = z.object({ - style: z.nativeEnum(BrandingStyle), - logoUrl: z.string().url(), + logoUrl: z.string().url().optional(), darkLogoUrl: z.string().url().optional(), - slogan: z.string().optional(), favicon: z.string().url().optional(), }); diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index 6a8453a52..800e20893 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -2,7 +2,7 @@ import { generateDarkColor } from '@logto/core-kit'; import type { SignInExperience } from '../db-entries/index.js'; import { SignInMode } from '../db-entries/index.js'; -import { BrandingStyle, SignInIdentifier } from '../foundations/index.js'; +import { SignInIdentifier } from '../foundations/index.js'; import { adminTenantId, defaultTenantId } from './tenant.js'; const defaultPrimaryColor = '#6139F6'; @@ -17,7 +17,6 @@ export const createDefaultSignInExperience = (forTenantId: string): Readonly }, signInMode: SignInMode.Register, branding: { - style: BrandingStyle.Logo_Slogan, logoUrl: 'https://logto.io/logo.svg', darkLogoUrl: 'https://logto.io/logo-dark.svg', - slogan: 'admin_console.welcome.title', }, }); diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index ebc7b8a7c..dcbd992cf 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -1,11 +1,5 @@ import type { SignInExperience, SignIn } from '@logto/schemas'; -import { - BrandingStyle, - ConnectorPlatform, - ConnectorType, - SignInIdentifier, - SignInMode, -} from '@logto/schemas'; +import { ConnectorPlatform, ConnectorType, SignInIdentifier, SignInMode } from '@logto/schemas'; import type { SignInExperienceResponse } from '@/types'; @@ -190,9 +184,7 @@ export const mockSignInExperience: SignInExperience = { darkPrimaryColor: '#fff', }, branding: { - style: BrandingStyle.Logo_Slogan, logoUrl: 'http://logto.png', - slogan: 'logto', }, termsOfUseUrl: 'http://terms.of.use/', privacyPolicyUrl: 'http://privacy.policy/', diff --git a/packages/ui/src/components/BrandingHeader/index.tsx b/packages/ui/src/components/BrandingHeader/index.tsx index 4247183a4..cab6d4617 100644 --- a/packages/ui/src/components/BrandingHeader/index.tsx +++ b/packages/ui/src/components/BrandingHeader/index.tsx @@ -1,3 +1,4 @@ +import type { Nullable } from '@silverhand/essentials'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import type { TFuncKey } from 'react-i18next'; @@ -6,7 +7,7 @@ import * as styles from './index.module.scss'; export type Props = { className?: string; - logo: string; + logo?: Nullable; headline?: TFuncKey; }; @@ -15,7 +16,7 @@ const BrandingHeader = ({ logo, headline, className }: Props) => { return (
- app logo + {logo && app logo} {headline &&
{t(headline)}
}
); diff --git a/packages/ui/src/containers/LandingPageContainer/index.tsx b/packages/ui/src/containers/LandingPageContainer/index.tsx index ba394a29e..a2075e5f5 100644 --- a/packages/ui/src/containers/LandingPageContainer/index.tsx +++ b/packages/ui/src/containers/LandingPageContainer/index.tsx @@ -5,7 +5,7 @@ import type { TFuncKey } from 'react-i18next'; import BrandingHeader from '@/components/BrandingHeader'; import { PageContext } from '@/hooks/use-page-context'; -import { getLogoUrl } from '@/utils/logo'; +import { getBrandingLogoUrl } from '@/utils/logo'; import AppNotification from '../AppNotification'; import * as styles from './index.module.scss'; @@ -23,7 +23,10 @@ const LandingPageContainer = ({ children, className, title }: Props) => { return null; } - const { logoUrl, darkLogoUrl } = experienceSettings.branding; + const { + color: { isDarkModeEnabled }, + branding, + } = experienceSettings; return ( <> @@ -32,7 +35,7 @@ const LandingPageContainer = ({ children, className, title }: Props) => { {children} diff --git a/packages/ui/src/hooks/use-preview.ts b/packages/ui/src/hooks/use-preview.ts index 753481dea..77ac05ddf 100644 --- a/packages/ui/src/hooks/use-preview.ts +++ b/packages/ui/src/hooks/use-preview.ts @@ -2,17 +2,26 @@ import { ConnectorPlatform } from '@logto/schemas'; import { conditionalString } from '@silverhand/essentials'; import { useEffect, useState } from 'react'; +import * as appStyles from '@/containers/AppBoundary/index.module.scss'; import * as styles from '@/containers/AppContent/index.module.scss'; import type { Context } from '@/hooks/use-page-context'; import initI18n from '@/i18n/init'; import { changeLanguage } from '@/i18n/utils'; -import type { SignInExperienceResponse, PreviewConfig } from '@/types'; +import type { SignInExperienceResponse, PreviewConfig, Theme } from '@/types'; import { parseQueryParameters } from '@/utils'; import { filterPreviewSocialConnectors } from '@/utils/social-connectors'; +const applyTheme = (theme: Theme) => { + document.body.classList.remove( + conditionalString(appStyles.light), + conditionalString(appStyles.dark) + ); + document.body.classList.add(conditionalString(appStyles[theme])); +}; + const usePreview = (context: Context): [boolean, PreviewConfig?] => { const [previewConfig, setPreviewConfig] = useState(); - const { setTheme, setExperienceSettings, setPlatform } = context; + const { setExperienceSettings, setPlatform } = context; const { preview } = parseQueryParameters(window.location.search); const isPreview = preview === 'true'; @@ -56,7 +65,7 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => { } const { - signInExperience: { socialConnectors, color, ...rest }, + signInExperience: { socialConnectors, ...rest }, mode, platform, isNative, @@ -64,10 +73,6 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => { const experienceSettings: SignInExperienceResponse = { ...rest, - color: { - ...color, - isDarkModeEnabled: false, // Disable theme mode auto detection on preview - }, socialConnectors: filterPreviewSocialConnectors( isNative ? ConnectorPlatform.Native : ConnectorPlatform.Web, socialConnectors @@ -75,13 +80,13 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => { }; (async () => { - setTheme(mode); + applyTheme(mode); setPlatform(platform); setExperienceSettings(experienceSettings); })(); - }, [isPreview, previewConfig, setExperienceSettings, setPlatform, setTheme]); + }, [isPreview, previewConfig, setExperienceSettings, setPlatform]); useEffect(() => { if (!isPreview || !previewConfig?.language) { diff --git a/packages/ui/src/pages/Consent/index.tsx b/packages/ui/src/pages/Consent/index.tsx index cacabb31d..baeed3fe4 100644 --- a/packages/ui/src/pages/Consent/index.tsx +++ b/packages/ui/src/pages/Consent/index.tsx @@ -1,3 +1,4 @@ +import { conditional } from '@silverhand/essentials'; import { useEffect, useContext, useState } from 'react'; import { consent } from '@/apis/consent'; @@ -5,7 +6,7 @@ import { LoadingIcon } from '@/components/LoadingLayer'; import useApi from '@/hooks/use-api'; import useErrorHandler from '@/hooks/use-error-handler'; import { PageContext } from '@/hooks/use-page-context'; -import { getLogoUrl } from '@/utils/logo'; +import { getBrandingLogoUrl } from '@/utils/logo'; import * as styles from './index.module.scss'; @@ -13,7 +14,13 @@ const Consent = () => { const { experienceSettings, theme } = useContext(PageContext); const handleError = useErrorHandler(); const asyncConsent = useApi(consent); - const branding = experienceSettings?.branding; + const { branding, color } = experienceSettings ?? {}; + const brandingLogo = conditional( + branding && + color && + getBrandingLogoUrl({ theme, branding, isDarkModeEnabled: color.isDarkModeEnabled }) + ); + const [loading, setLoading] = useState(true); useEffect(() => { @@ -35,12 +42,7 @@ const Consent = () => { return (
- {branding && ( - logo - )} + {brandingLogo && logo} {loading && }
); diff --git a/packages/ui/src/utils/logo.ts b/packages/ui/src/utils/logo.ts index 5fe96d767..c37b7d36b 100644 --- a/packages/ui/src/utils/logo.ts +++ b/packages/ui/src/utils/logo.ts @@ -1,3 +1,4 @@ +import type { Branding } from '@logto/schemas'; import type { Nullable } from '@silverhand/essentials'; import type { Theme } from '@/types'; @@ -16,3 +17,27 @@ export const getLogoUrl = ({ theme, logoUrl, darkLogoUrl, isApple }: GetLogoUrl) return logoUrl; }; + +export type GetBrandingLogoUrl = { + theme: Theme; + branding: Branding; + isDarkModeEnabled: boolean; +}; + +export const getBrandingLogoUrl = ({ theme, branding, isDarkModeEnabled }: GetBrandingLogoUrl) => { + const { logoUrl, darkLogoUrl } = branding; + + if (!isDarkModeEnabled) { + return logoUrl; + } + + if (!logoUrl && !darkLogoUrl) { + return null; + } + + if (logoUrl && darkLogoUrl) { + return getLogoUrl({ theme, logoUrl, darkLogoUrl }); + } + + return logoUrl ?? darkLogoUrl; +};