mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(console): sie form reorg (#1218)
This commit is contained in:
parent
9aca422578
commit
2c413341d1
21 changed files with 174 additions and 89 deletions
|
@ -73,7 +73,7 @@ const Main = () => {
|
||||||
<Route path=":userId/logs/:logId" element={<AuditLogDetails />} />
|
<Route path=":userId/logs/:logId" element={<AuditLogDetails />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="sign-in-experience">
|
<Route path="sign-in-experience">
|
||||||
<Route index element={<Navigate replace to="experience" />} />
|
<Route index element={<Navigate replace to="branding" />} />
|
||||||
<Route path=":tab" element={<SignInExperience />} />
|
<Route path=":tab" element={<SignInExperience />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
|
|
|
@ -3,10 +3,8 @@ import React from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ColorPicker from '@/components/ColorPicker';
|
|
||||||
import FormField from '@/components/FormField';
|
import FormField from '@/components/FormField';
|
||||||
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
import RadioGroup, { Radio } from '@/components/RadioGroup';
|
||||||
import Switch from '@/components/Switch';
|
|
||||||
import TextInput from '@/components/TextInput';
|
import TextInput from '@/components/TextInput';
|
||||||
import { uriValidator } from '@/utilities/validator';
|
import { uriValidator } from '@/utilities/validator';
|
||||||
|
|
||||||
|
@ -22,39 +20,13 @@ const BrandingForm = () => {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext<SignInExperienceForm>();
|
} = useFormContext<SignInExperienceForm>();
|
||||||
|
|
||||||
const isDarkModeEnabled = watch('branding.isDarkModeEnabled');
|
const isDarkModeEnabled = watch('color.isDarkModeEnabled');
|
||||||
const style = watch('branding.style');
|
const style = watch('branding.style');
|
||||||
const isSloganRequired = style === BrandingStyle.Logo_Slogan;
|
const isSloganRequired = style === BrandingStyle.Logo_Slogan;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.title}>{t('sign_in_exp.branding.title')}</div>
|
<div className={styles.title}>{t('sign_in_exp.branding.title')}</div>
|
||||||
<FormField title="admin_console.sign_in_exp.branding.primary_color">
|
|
||||||
<Controller
|
|
||||||
name="branding.primaryColor"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<ColorPicker value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField title="admin_console.sign_in_exp.branding.dark_mode">
|
|
||||||
<Switch
|
|
||||||
label={t('sign_in_exp.branding.dark_mode_description')}
|
|
||||||
{...register('branding.isDarkModeEnabled')}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
{isDarkModeEnabled && (
|
|
||||||
<FormField title="admin_console.sign_in_exp.branding.dark_primary_color">
|
|
||||||
<Controller
|
|
||||||
name="branding.darkPrimaryColor"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<ColorPicker value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
)}
|
|
||||||
<FormField title="admin_console.sign_in_exp.branding.ui_style">
|
<FormField title="admin_console.sign_in_exp.branding.ui_style">
|
||||||
<Controller
|
<Controller
|
||||||
name="branding.style"
|
name="branding.style"
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import ColorPicker from '@/components/ColorPicker';
|
||||||
|
import FormField from '@/components/FormField';
|
||||||
|
import Switch from '@/components/Switch';
|
||||||
|
|
||||||
|
import { SignInExperienceForm } from '../types';
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
const ColorForm = () => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const { watch, register, control } = useFormContext<SignInExperienceForm>();
|
||||||
|
|
||||||
|
const isDarkModeEnabled = watch('color.isDarkModeEnabled');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.title}>{t('sign_in_exp.color.title')}</div>
|
||||||
|
<FormField title="admin_console.sign_in_exp.color.primary_color">
|
||||||
|
<Controller
|
||||||
|
name="color.primaryColor"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ColorPicker value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField title="admin_console.sign_in_exp.color.dark_mode">
|
||||||
|
<Switch
|
||||||
|
label={t('sign_in_exp.color.dark_mode_description')}
|
||||||
|
{...register('color.isDarkModeEnabled')}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{isDarkModeEnabled && (
|
||||||
|
<FormField title="admin_console.sign_in_exp.color.dark_primary_color">
|
||||||
|
<Controller
|
||||||
|
name="color.darkPrimaryColor"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ColorPicker value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorForm;
|
|
@ -20,6 +20,7 @@ import usePreviewConfigs from '../hooks';
|
||||||
import { SignInExperienceForm } from '../types';
|
import { SignInExperienceForm } from '../types';
|
||||||
import { signInExperienceParser } from '../utilities';
|
import { signInExperienceParser } from '../utilities';
|
||||||
import BrandingForm from './BrandingForm';
|
import BrandingForm from './BrandingForm';
|
||||||
|
import ColorForm from './ColorForm';
|
||||||
import * as styles from './GuideModal.module.scss';
|
import * as styles from './GuideModal.module.scss';
|
||||||
import LanguagesForm from './LanguagesForm';
|
import LanguagesForm from './LanguagesForm';
|
||||||
import Preview from './Preview';
|
import Preview from './Preview';
|
||||||
|
@ -100,6 +101,9 @@ const GuideModal = ({ isOpen, onClose }: Props) => {
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<div className={styles.form}>
|
<div className={styles.form}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<ColorForm />
|
||||||
|
</div>
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<BrandingForm />
|
<BrandingForm />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import useSettings from '@/hooks/use-settings';
|
||||||
import * as detailsStyles from '@/scss/details.module.scss';
|
import * as detailsStyles from '@/scss/details.module.scss';
|
||||||
|
|
||||||
import BrandingForm from './components/BrandingForm';
|
import BrandingForm from './components/BrandingForm';
|
||||||
|
import ColorForm from './components/ColorForm';
|
||||||
import LanguagesForm from './components/LanguagesForm';
|
import LanguagesForm from './components/LanguagesForm';
|
||||||
import Preview from './components/Preview';
|
import Preview from './components/Preview';
|
||||||
import SignInMethodsChangePreview from './components/SignInMethodsChangePreview';
|
import SignInMethodsChangePreview from './components/SignInMethodsChangePreview';
|
||||||
|
@ -100,8 +101,8 @@ const SignInExperience = () => {
|
||||||
<Card className={styles.card}>
|
<Card className={styles.card}>
|
||||||
<CardTitle title="sign_in_exp.title" subtitle="sign_in_exp.description" />
|
<CardTitle title="sign_in_exp.title" subtitle="sign_in_exp.description" />
|
||||||
<TabNav className={styles.tabs}>
|
<TabNav className={styles.tabs}>
|
||||||
<TabNavItem href="/sign-in-experience/experience">
|
<TabNavItem href="/sign-in-experience/branding">
|
||||||
{t('sign_in_exp.tabs.experience')}
|
{t('sign_in_exp.tabs.branding')}
|
||||||
</TabNavItem>
|
</TabNavItem>
|
||||||
<TabNavItem href="/sign-in-experience/methods">
|
<TabNavItem href="/sign-in-experience/methods">
|
||||||
{t('sign_in_exp.tabs.methods')}
|
{t('sign_in_exp.tabs.methods')}
|
||||||
|
@ -116,14 +117,19 @@ const SignInExperience = () => {
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className={classNames(detailsStyles.body, styles.form)}>
|
<div className={classNames(detailsStyles.body, styles.form)}>
|
||||||
{tab === 'experience' && (
|
{tab === 'branding' && (
|
||||||
<>
|
<>
|
||||||
|
<ColorForm />
|
||||||
<BrandingForm />
|
<BrandingForm />
|
||||||
<TermsForm />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{tab === 'methods' && <SignInMethodsForm />}
|
{tab === 'methods' && <SignInMethodsForm />}
|
||||||
{tab === 'others' && <LanguagesForm />}
|
{tab === 'others' && (
|
||||||
|
<>
|
||||||
|
<TermsForm />
|
||||||
|
<LanguagesForm />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={detailsStyles.footer}>
|
<div className={detailsStyles.footer}>
|
||||||
<div className={detailsStyles.footerMain}>
|
<div className={detailsStyles.footerMain}>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
SignInMethods,
|
SignInMethods,
|
||||||
SignInMethodState,
|
SignInMethodState,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
import { conditional } from '@silverhand/essentials';
|
||||||
|
|
||||||
import { LanguageMode, SignInExperienceForm } from './types';
|
import { LanguageMode, SignInExperienceForm } from './types';
|
||||||
|
|
||||||
|
@ -57,18 +58,23 @@ export const signInExperienceParser = {
|
||||||
},
|
},
|
||||||
toRemoteModel: (setup: SignInExperienceForm): SignInExperience => {
|
toRemoteModel: (setup: SignInExperienceForm): SignInExperience => {
|
||||||
const {
|
const {
|
||||||
|
color,
|
||||||
branding,
|
branding,
|
||||||
languageInfo: { mode, fallbackLanguage, fixedLanguage },
|
languageInfo: { mode, fallbackLanguage, fixedLanguage },
|
||||||
} = setup;
|
} = setup;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...setup,
|
...setup,
|
||||||
|
color: {
|
||||||
|
...color,
|
||||||
|
// Transform empty string to undefined
|
||||||
|
darkPrimaryColor: conditional(color.darkPrimaryColor?.length && color.darkPrimaryColor),
|
||||||
|
},
|
||||||
branding: {
|
branding: {
|
||||||
...branding,
|
...branding,
|
||||||
// Transform empty string to undefined
|
// Transform empty string to undefined
|
||||||
darkPrimaryColor: branding.darkPrimaryColor?.length ? branding.darkPrimaryColor : undefined,
|
darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl),
|
||||||
darkLogoUrl: branding.darkLogoUrl?.length ? branding.darkLogoUrl : undefined,
|
slogan: conditional(branding.slogan?.length && branding.slogan),
|
||||||
slogan: branding.slogan?.length ? branding.slogan : undefined,
|
|
||||||
},
|
},
|
||||||
signInMethods: {
|
signInMethods: {
|
||||||
username: findMethodState(setup, 'username'),
|
username: findMethodState(setup, 'username'),
|
||||||
|
|
|
@ -8,14 +8,17 @@ import {
|
||||||
SignInMethodState,
|
SignInMethodState,
|
||||||
TermsOfUse,
|
TermsOfUse,
|
||||||
SignInMode,
|
SignInMode,
|
||||||
|
Color,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
|
||||||
export const mockSignInExperience: SignInExperience = {
|
export const mockSignInExperience: SignInExperience = {
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
branding: {
|
color: {
|
||||||
primaryColor: '#000',
|
primaryColor: '#000',
|
||||||
isDarkModeEnabled: true,
|
isDarkModeEnabled: true,
|
||||||
darkPrimaryColor: '#fff',
|
darkPrimaryColor: '#fff',
|
||||||
|
},
|
||||||
|
branding: {
|
||||||
style: BrandingStyle.Logo,
|
style: BrandingStyle.Logo,
|
||||||
logoUrl: 'http://logto.png',
|
logoUrl: 'http://logto.png',
|
||||||
slogan: 'logto',
|
slogan: 'logto',
|
||||||
|
@ -38,10 +41,13 @@ export const mockSignInExperience: SignInExperience = {
|
||||||
signInMode: SignInMode.SignInAndRegister,
|
signInMode: SignInMode.SignInAndRegister,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockBranding: Branding = {
|
export const mockColor: Color = {
|
||||||
primaryColor: '#000',
|
primaryColor: '#000',
|
||||||
isDarkModeEnabled: true,
|
isDarkModeEnabled: true,
|
||||||
darkPrimaryColor: '#fff',
|
darkPrimaryColor: '#fff',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockBranding: Branding = {
|
||||||
style: BrandingStyle.Logo_Slogan,
|
style: BrandingStyle.Logo_Slogan,
|
||||||
logoUrl: 'http://silverhand.png',
|
logoUrl: 'http://silverhand.png',
|
||||||
slogan: 'Silverhand.',
|
slogan: 'Silverhand.',
|
||||||
|
|
|
@ -21,6 +21,7 @@ describe('sign-in-experience query', () => {
|
||||||
|
|
||||||
const dbvalue = {
|
const dbvalue = {
|
||||||
...mockSignInExperience,
|
...mockSignInExperience,
|
||||||
|
color: JSON.stringify(mockSignInExperience.color),
|
||||||
branding: JSON.stringify(mockSignInExperience.branding),
|
branding: JSON.stringify(mockSignInExperience.branding),
|
||||||
termsOfUse: JSON.stringify(mockSignInExperience.termsOfUse),
|
termsOfUse: JSON.stringify(mockSignInExperience.termsOfUse),
|
||||||
languageInfo: JSON.stringify(mockSignInExperience.languageInfo),
|
languageInfo: JSON.stringify(mockSignInExperience.languageInfo),
|
||||||
|
@ -31,7 +32,7 @@ describe('sign-in-experience query', () => {
|
||||||
it('findDefaultSignInExperience', async () => {
|
it('findDefaultSignInExperience', async () => {
|
||||||
/* eslint-disable sql/no-unsafe-query */
|
/* eslint-disable sql/no-unsafe-query */
|
||||||
const expectSql = `
|
const expectSql = `
|
||||||
select "id", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_targets", "sign_in_mode"
|
select "id", "color", "branding", "language_info", "terms_of_use", "sign_in_methods", "social_sign_in_connector_targets", "sign_in_mode"
|
||||||
from "sign_in_experiences"
|
from "sign_in_experiences"
|
||||||
where "id" = $1
|
where "id" = $1
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -26,30 +26,6 @@ const expectPatchResponseStatus = async (signInExperience: any, status: number)
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('branding', () => {
|
describe('branding', () => {
|
||||||
const colorKeys = ['primaryColor', 'darkPrimaryColor'];
|
|
||||||
const invalidColors = [null, '#0'];
|
|
||||||
|
|
||||||
describe('colors', () => {
|
|
||||||
test.each(invalidColors)('should fail when color is %p', async (invalidColor) => {
|
|
||||||
for (const colorKey of colorKeys) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await expectPatchResponseStatus(
|
|
||||||
{ branding: { ...mockBranding, [colorKey]: invalidColor } },
|
|
||||||
400
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
it('should succeed when color is valid', async () => {
|
|
||||||
for (const colorKey of colorKeys) {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await expectPatchResponseStatus(
|
|
||||||
{ branding: { ...mockBranding, [colorKey]: '#169deF' } },
|
|
||||||
200
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('style', () => {
|
describe('style', () => {
|
||||||
test.each(Object.values(BrandingStyle))('%p should succeed', async (style) => {
|
test.each(Object.values(BrandingStyle))('%p should succeed', async (style) => {
|
||||||
const signInExperience = { branding: { ...mockBranding, style } };
|
const signInExperience = { branding: { ...mockBranding, style } };
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { CreateSignInExperience, SignInExperience } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { mockColor, mockSignInExperience } from '@/__mocks__';
|
||||||
|
import { createRequester } from '@/utils/test-utils';
|
||||||
|
|
||||||
|
import signInExperiencesRoutes from './sign-in-experience';
|
||||||
|
|
||||||
|
jest.mock('@/queries/sign-in-experience', () => ({
|
||||||
|
updateDefaultSignInExperience: jest.fn(
|
||||||
|
async (data: Partial<CreateSignInExperience>): Promise<SignInExperience> => ({
|
||||||
|
...mockSignInExperience,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/connectors', () => ({
|
||||||
|
getConnectorInstances: jest.fn(async () => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes });
|
||||||
|
|
||||||
|
const expectPatchResponseStatus = async (signInExperience: any, status: number) => {
|
||||||
|
const response = await signInExperienceRequester.patch('/sign-in-exp').send(signInExperience);
|
||||||
|
expect(response.status).toEqual(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorKeys = ['primaryColor', 'darkPrimaryColor'];
|
||||||
|
const invalidColors = [null, '#0'];
|
||||||
|
|
||||||
|
describe('colors', () => {
|
||||||
|
test.each(invalidColors)('should fail when color is %p', async (invalidColor) => {
|
||||||
|
for (const colorKey of colorKeys) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await expectPatchResponseStatus({ color: { ...mockColor, [colorKey]: invalidColor } }, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should succeed when color is valid', async () => {
|
||||||
|
for (const colorKey of colorKeys) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await expectPatchResponseStatus({ color: { ...mockColor, [colorKey]: '#169deF' } }, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,6 +13,7 @@ import {
|
||||||
mockSignInExperience,
|
mockSignInExperience,
|
||||||
mockSignInMethods,
|
mockSignInMethods,
|
||||||
mockWechatConnectorInstance,
|
mockWechatConnectorInstance,
|
||||||
|
mockColor,
|
||||||
} from '@/__mocks__';
|
} from '@/__mocks__';
|
||||||
import * as signInExpLib from '@/lib/sign-in-experience';
|
import * as signInExpLib from '@/lib/sign-in-experience';
|
||||||
import { createRequester } from '@/utils/test-utils';
|
import { createRequester } from '@/utils/test-utils';
|
||||||
|
@ -142,6 +143,7 @@ describe('PATCH /sign-in-exp', () => {
|
||||||
const validateSignInMethods = jest.spyOn(signInExpLib, 'validateSignInMethods');
|
const validateSignInMethods = jest.spyOn(signInExpLib, 'validateSignInMethods');
|
||||||
|
|
||||||
const response = await signInExperienceRequester.patch('/sign-in-exp').send({
|
const response = await signInExperienceRequester.patch('/sign-in-exp').send({
|
||||||
|
color: mockColor,
|
||||||
branding: mockBranding,
|
branding: mockBranding,
|
||||||
termsOfUse,
|
termsOfUse,
|
||||||
signInMethods: mockSignInMethods,
|
signInMethods: mockSignInMethods,
|
||||||
|
@ -160,6 +162,7 @@ describe('PATCH /sign-in-exp', () => {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: {
|
body: {
|
||||||
...mockSignInExperience,
|
...mockSignInExperience,
|
||||||
|
color: mockColor,
|
||||||
branding: mockBranding,
|
branding: mockBranding,
|
||||||
termsOfUse,
|
termsOfUse,
|
||||||
signInMethods: mockSignInMethods,
|
signInMethods: mockSignInMethods,
|
||||||
|
|
|
@ -406,7 +406,7 @@ const translation = {
|
||||||
title: 'Sign-in Experience',
|
title: 'Sign-in Experience',
|
||||||
description: 'Customize the sign in UI to match your brand and preview in real time.',
|
description: 'Customize the sign in UI to match your brand and preview in real time.',
|
||||||
tabs: {
|
tabs: {
|
||||||
experience: 'Experience',
|
branding: 'Branding',
|
||||||
methods: 'Sign in methods',
|
methods: 'Sign in methods',
|
||||||
others: 'Others',
|
others: 'Others',
|
||||||
},
|
},
|
||||||
|
@ -418,14 +418,17 @@ const translation = {
|
||||||
'Please note that sign-in experience will apply to all applications under this account.',
|
'Please note that sign-in experience will apply to all applications under this account.',
|
||||||
got_it: 'Got it',
|
got_it: 'Got it',
|
||||||
},
|
},
|
||||||
branding: {
|
color: {
|
||||||
title: 'BRANDING',
|
title: 'COLOR',
|
||||||
primary_color: 'Brand color',
|
primary_color: 'Brand color',
|
||||||
dark_primary_color: 'Brand color (Dark)',
|
dark_primary_color: 'Brand color (Dark)',
|
||||||
dark_mode: 'Enable dark mode',
|
dark_mode: 'Enable dark mode',
|
||||||
dark_mode_description:
|
dark_mode_description:
|
||||||
'Your app will have an auto-generated dark mode theme based on your brand color and Logto algorithm. You are free to customize.',
|
'Your app will have an auto-generated dark mode theme based on your brand color and Logto algorithm. You are free to customize.',
|
||||||
ui_style: 'Branding area',
|
},
|
||||||
|
branding: {
|
||||||
|
title: 'BRANDING AREA',
|
||||||
|
ui_style: 'Style',
|
||||||
styles: {
|
styles: {
|
||||||
logo_slogan: 'App logo with slogan',
|
logo_slogan: 'App logo with slogan',
|
||||||
logo: 'App logo',
|
logo: 'App logo',
|
||||||
|
|
|
@ -396,7 +396,7 @@ const translation = {
|
||||||
title: '登录体验',
|
title: '登录体验',
|
||||||
description: '自定义登录界面,并实时预览真实效果。',
|
description: '自定义登录界面,并实时预览真实效果。',
|
||||||
tabs: {
|
tabs: {
|
||||||
experience: '体验',
|
branding: '品牌',
|
||||||
methods: '登录方式',
|
methods: '登录方式',
|
||||||
others: '其它',
|
others: '其它',
|
||||||
},
|
},
|
||||||
|
@ -406,14 +406,17 @@ const translation = {
|
||||||
apply_remind: '请注意,登录体验将会应用到当前账户下的所有应用。',
|
apply_remind: '请注意,登录体验将会应用到当前账户下的所有应用。',
|
||||||
got_it: '知道了',
|
got_it: '知道了',
|
||||||
},
|
},
|
||||||
branding: {
|
color: {
|
||||||
title: '品牌',
|
title: '颜色',
|
||||||
primary_color: '品牌颜色',
|
primary_color: '品牌颜色',
|
||||||
dark_primary_color: '品牌颜色 (暗黑)',
|
dark_primary_color: '品牌颜色 (深色)',
|
||||||
dark_mode: '开启暗黑模式',
|
dark_mode: '开启深色模式',
|
||||||
dark_mode_description:
|
dark_mode_description:
|
||||||
'基于你的品牌颜色和 Logto 的算法,你的应用将会有一个自动生成的暗黑模式。当然,你可以自定义和修改。',
|
'基于你的品牌颜色和 Logto 的算法,你的应用将会有一个自动生成的深色模式。当然,你可以自定义和修改。',
|
||||||
ui_style: '品牌定制区',
|
},
|
||||||
|
branding: {
|
||||||
|
title: '品牌定制区',
|
||||||
|
ui_style: '样式',
|
||||||
styles: {
|
styles: {
|
||||||
logo_slogan: 'App logo 和标语',
|
logo_slogan: 'App logo 和标语',
|
||||||
logo: '仅有Logo',
|
logo: '仅有Logo',
|
||||||
|
|
|
@ -72,15 +72,20 @@ export type Identities = z.infer<typeof identitiesGuard>;
|
||||||
* SignIn Experiences
|
* SignIn Experiences
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const colorGuard = z.object({
|
||||||
|
primaryColor: z.string().regex(hexColorRegEx),
|
||||||
|
isDarkModeEnabled: z.boolean(),
|
||||||
|
darkPrimaryColor: z.string().regex(hexColorRegEx).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Color = z.infer<typeof colorGuard>;
|
||||||
|
|
||||||
export enum BrandingStyle {
|
export enum BrandingStyle {
|
||||||
Logo = 'Logo',
|
Logo = 'Logo',
|
||||||
Logo_Slogan = 'Logo_Slogan',
|
Logo_Slogan = 'Logo_Slogan',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const brandingGuard = z.object({
|
export const brandingGuard = z.object({
|
||||||
primaryColor: z.string().regex(hexColorRegEx),
|
|
||||||
isDarkModeEnabled: z.boolean(),
|
|
||||||
darkPrimaryColor: z.string().regex(hexColorRegEx).optional(),
|
|
||||||
style: z.nativeEnum(BrandingStyle),
|
style: z.nativeEnum(BrandingStyle),
|
||||||
logoUrl: z.string().url(),
|
logoUrl: z.string().url(),
|
||||||
darkLogoUrl: z.string().url().optional(),
|
darkLogoUrl: z.string().url().optional(),
|
||||||
|
|
|
@ -5,10 +5,12 @@ import { BrandingStyle, SignInMethodState } from '../foundations';
|
||||||
|
|
||||||
export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
|
export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
branding: {
|
color: {
|
||||||
primaryColor: '#6139F6',
|
primaryColor: '#6139F6',
|
||||||
isDarkModeEnabled: false,
|
isDarkModeEnabled: false,
|
||||||
darkPrimaryColor: '#6139F6',
|
darkPrimaryColor: '#6139F6',
|
||||||
|
},
|
||||||
|
branding: {
|
||||||
style: BrandingStyle.Logo,
|
style: BrandingStyle.Logo,
|
||||||
logoUrl: 'https://logto.io/logo.svg',
|
logoUrl: 'https://logto.io/logo.svg',
|
||||||
darkLogoUrl: 'https://logto.io/logo.svg',
|
darkLogoUrl: 'https://logto.io/logo.svg',
|
||||||
|
|
|
@ -2,6 +2,7 @@ create type sign_in_mode as enum ('SignIn', 'Register', 'SignInAndRegister');
|
||||||
|
|
||||||
create table sign_in_experiences (
|
create table sign_in_experiences (
|
||||||
id varchar(21) not null,
|
id varchar(21) not null,
|
||||||
|
color jsonb /* @use Color */ not null,
|
||||||
branding jsonb /* @use Branding */ not null,
|
branding jsonb /* @use Branding */ not null,
|
||||||
language_info jsonb /* @use LanguageInfo */ not null,
|
language_info jsonb /* @use LanguageInfo */ not null,
|
||||||
terms_of_use jsonb /* @use TermsOfUse */ not null,
|
terms_of_use jsonb /* @use TermsOfUse */ not null,
|
||||||
|
|
|
@ -123,10 +123,12 @@ export const mockSocialConnectorData = {
|
||||||
|
|
||||||
export const mockSignInExperience: SignInExperience = {
|
export const mockSignInExperience: SignInExperience = {
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
branding: {
|
color: {
|
||||||
primaryColor: '#000',
|
primaryColor: '#000',
|
||||||
isDarkModeEnabled: true,
|
isDarkModeEnabled: true,
|
||||||
darkPrimaryColor: '#fff',
|
darkPrimaryColor: '#fff',
|
||||||
|
},
|
||||||
|
branding: {
|
||||||
style: BrandingStyle.Logo_Slogan,
|
style: BrandingStyle.Logo_Slogan,
|
||||||
logoUrl: 'http://logto.png',
|
logoUrl: 'http://logto.png',
|
||||||
slogan: 'logto',
|
slogan: 'logto',
|
||||||
|
@ -151,6 +153,7 @@ export const mockSignInExperience: SignInExperience = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockSignInExperienceSettings: SignInExperienceSettings = {
|
export const mockSignInExperienceSettings: SignInExperienceSettings = {
|
||||||
|
color: mockSignInExperience.color,
|
||||||
branding: mockSignInExperience.branding,
|
branding: mockSignInExperience.branding,
|
||||||
termsOfUse: mockSignInExperience.termsOfUse,
|
termsOfUse: mockSignInExperience.termsOfUse,
|
||||||
languageInfo: mockSignInExperience.languageInfo,
|
languageInfo: mockSignInExperience.languageInfo,
|
||||||
|
|
|
@ -22,10 +22,7 @@ const AppContent = ({ children }: Props) => {
|
||||||
}, [setToast]);
|
}, [setToast]);
|
||||||
|
|
||||||
// Set Primary Color
|
// Set Primary Color
|
||||||
useColorTheme(
|
useColorTheme(experienceSettings?.color.primaryColor, experienceSettings?.color.darkPrimaryColor);
|
||||||
experienceSettings?.branding.primaryColor,
|
|
||||||
experienceSettings?.branding.darkPrimaryColor
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set Theme Mode
|
// Set Theme Mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -60,7 +60,7 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
signInExperience: { signInMethods, socialConnectors, branding, ...rest },
|
signInExperience: { signInMethods, socialConnectors, color, ...rest },
|
||||||
language,
|
language,
|
||||||
mode,
|
mode,
|
||||||
platform,
|
platform,
|
||||||
|
@ -68,8 +68,8 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
||||||
|
|
||||||
const experienceSettings: SignInExperienceSettings = {
|
const experienceSettings: SignInExperienceSettings = {
|
||||||
...rest,
|
...rest,
|
||||||
branding: {
|
color: {
|
||||||
...branding,
|
...color,
|
||||||
isDarkModeEnabled: false, // Disable theme mode auto detection on preview
|
isDarkModeEnabled: false, // Disable theme mode auto detection on preview
|
||||||
},
|
},
|
||||||
primarySignInMethod: getPrimarySignInMethod(signInMethods),
|
primarySignInMethod: getPrimarySignInMethod(signInMethods),
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function useTheme(): Theme {
|
||||||
const { experienceSettings, theme, setTheme } = useContext(PageContext);
|
const { experienceSettings, theme, setTheme } = useContext(PageContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!experienceSettings?.branding.isDarkModeEnabled) {
|
if (!experienceSettings?.color.isDarkModeEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
SignInExperience,
|
SignInExperience,
|
||||||
ConnectorMetadata,
|
ConnectorMetadata,
|
||||||
SignInMode,
|
SignInMode,
|
||||||
|
Color,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
|
||||||
export type UserFlow = 'sign-in' | 'register';
|
export type UserFlow = 'sign-in' | 'register';
|
||||||
|
@ -30,6 +31,7 @@ export type SignInExperienceSettingsResponse = SignInExperience & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignInExperienceSettings = {
|
export type SignInExperienceSettings = {
|
||||||
|
color: Color;
|
||||||
branding: Branding;
|
branding: Branding;
|
||||||
languageInfo: LanguageInfo;
|
languageInfo: LanguageInfo;
|
||||||
termsOfUse: TermsOfUse;
|
termsOfUse: TermsOfUse;
|
||||||
|
|
Loading…
Add table
Reference in a new issue