mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor: remove branding style config (#3326)
This commit is contained in:
parent
95b6fb2613
commit
105390f004
35 changed files with 198 additions and 274 deletions
9
.changeset-staged/nice-lions-dream.md
Normal file
9
.changeset-staged/nice-lions-dream.md
Normal file
|
@ -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
|
|
@ -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<SignInExperienceForm>();
|
||||
|
||||
const isDarkModeEnabled = watch('color.isDarkModeEnabled');
|
||||
const style = watch('branding.style');
|
||||
const isSloganRequired = style === BrandingStyle.Logo_Slogan;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className={styles.title}>{t('sign_in_exp.branding.title')}</div>
|
||||
|
||||
<FormField title="sign_in_exp.branding.favicon">
|
||||
<TextInput
|
||||
{...register('branding.favicon', {
|
||||
|
@ -37,26 +33,9 @@ const BrandingForm = () => {
|
|||
placeholder={t('sign_in_exp.branding.favicon')}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="sign_in_exp.branding.ui_style">
|
||||
<Controller
|
||||
name="branding.style"
|
||||
control={control}
|
||||
defaultValue={BrandingStyle.Logo_Slogan}
|
||||
render={({ field: { onChange, value, name } }) => (
|
||||
<RadioGroup value={value} name={name} className={styles.radioGroup} onChange={onChange}>
|
||||
<Radio
|
||||
value={BrandingStyle.Logo_Slogan}
|
||||
title="sign_in_exp.branding.styles.logo_slogan"
|
||||
/>
|
||||
<Radio value={BrandingStyle.Logo} title="sign_in_exp.branding.styles.logo" />
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isRequired title="sign_in_exp.branding.logo_image_url">
|
||||
<FormField title="sign_in_exp.branding.logo_image_url">
|
||||
<TextInput
|
||||
{...register('branding.logoUrl', {
|
||||
required: true,
|
||||
validate: (value) => !value || uriValidator(value) || t('errors.invalid_uri_format'),
|
||||
})}
|
||||
hasError={Boolean(errors.branding?.logoUrl)}
|
||||
|
@ -76,15 +55,6 @@ const BrandingForm = () => {
|
|||
/>
|
||||
</FormField>
|
||||
)}
|
||||
{isSloganRequired && (
|
||||
<FormField isRequired={isSloganRequired} title="sign_in_exp.branding.slogan">
|
||||
<TextInput
|
||||
{...register('branding.slogan', { required: isSloganRequired })}
|
||||
hasError={Boolean(errors.branding?.slogan)}
|
||||
placeholder={t('sign_in_exp.branding.slogan_placeholder')}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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<typeof createSignInExperienceLibrary>;
|
||||
|
||||
export const createSignInExperienceLibrary = (
|
||||
|
@ -61,16 +52,7 @@ export const createSignInExperienceLibrary = (
|
|||
});
|
||||
};
|
||||
|
||||
const getSignInExperience = async (): Promise<SignInExperience> => {
|
||||
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<SignInExperience> => findDefaultSignInExperience();
|
||||
|
||||
return {
|
||||
validateLanguageInfo,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<T extends AuthedRouter>(
|
|||
}),
|
||||
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);
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: '이메일 주소',
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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ı.',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: '主要的登录方式必须有且仅有一个,请检查你的输入。',
|
||||
|
|
|
@ -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: '邮件地址',
|
||||
|
|
|
@ -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<DeprecatedSignInExperience>(
|
||||
sql`select * from sign_in_experiences`
|
||||
);
|
||||
|
||||
await Promise.all(rows.map(async (row) => alterBranding(row, pool)));
|
||||
},
|
||||
down: async (pool) => {
|
||||
const rows = await pool.many<SignInExperience>(sql`select * from sign_in_experiences`);
|
||||
|
||||
await Promise.all(rows.map(async (row) => rollbackBranding(row, pool)));
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -103,16 +103,9 @@ export const colorGuard = z.object({
|
|||
|
||||
export type Color = z.infer<typeof colorGuard>;
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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<Sig
|
|||
darkPrimaryColor: generateDarkColor(defaultPrimaryColor),
|
||||
},
|
||||
branding: {
|
||||
style: BrandingStyle.Logo,
|
||||
logoUrl: 'https://logto.io/logo.svg',
|
||||
darkLogoUrl: 'https://logto.io/logo-dark.svg',
|
||||
},
|
||||
|
@ -60,9 +59,7 @@ export const createAdminTenantSignInExperience = (): Readonly<SignInExperience>
|
|||
},
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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/',
|
||||
|
|
|
@ -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<string>;
|
||||
headline?: TFuncKey;
|
||||
};
|
||||
|
||||
|
@ -15,7 +16,7 @@ const BrandingHeader = ({ logo, headline, className }: Props) => {
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<img className={styles.logo} alt="app logo" src={logo} />
|
||||
{logo && <img className={styles.logo} alt="app logo" src={logo} />}
|
||||
{headline && <div className={styles.headline}>{t(headline)}</div>}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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) => {
|
|||
<BrandingHeader
|
||||
className={styles.header}
|
||||
headline={title}
|
||||
logo={getLogoUrl({ theme, logoUrl, darkLogoUrl })}
|
||||
logo={getBrandingLogoUrl({ theme, branding, isDarkModeEnabled })}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -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<PreviewConfig>();
|
||||
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) {
|
||||
|
|
|
@ -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 (
|
||||
<div className={styles.wrapper}>
|
||||
{branding && (
|
||||
<img
|
||||
alt="logo"
|
||||
src={getLogoUrl({ theme, logoUrl: branding.logoUrl, darkLogoUrl: branding.darkLogoUrl })}
|
||||
/>
|
||||
)}
|
||||
{brandingLogo && <img alt="logo" src={brandingLogo} />}
|
||||
{loading && <LoadingIcon />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue