diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx index 7c1727f1d..095905915 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import Card from '@/components/Card'; import FormField from '@/components/FormField'; -import Switch from '@/components/Switch'; import TextInput from '@/components/TextInput'; import { uriValidator } from '@/utilities/validator'; @@ -13,38 +12,26 @@ import * as styles from '../index.module.scss'; const TermsForm = () => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { - watch, register, formState: { errors }, } = useFormContext(); - const enabled = watch('termsOfUse.enabled'); return (
{t('sign_in_exp.others.terms_of_use.title')}
- - + !value || uriValidator(value) || t('errors.invalid_uri_format'), + })} + hasError={Boolean(errors.termsOfUseUrl)} + errorMessage={errors.termsOfUseUrl?.message} + placeholder={t('sign_in_exp.others.terms_of_use.terms_of_use_placeholder')} /> - {enabled && ( - - !value || uriValidator(value) || t('errors.invalid_uri_format'), - })} - hasError={Boolean(errors.termsOfUse)} - errorMessage={errors.termsOfUse?.contentUrl?.message} - placeholder={t('sign_in_exp.others.terms_of_use.terms_of_use_placeholder')} - /> - - )}
); }; diff --git a/packages/console/src/pages/SignInExperience/utils/form.ts b/packages/console/src/pages/SignInExperience/utils/form.ts index 65fb0c653..c97404550 100644 --- a/packages/console/src/pages/SignInExperience/utils/form.ts +++ b/packages/console/src/pages/SignInExperience/utils/form.ts @@ -113,12 +113,5 @@ export const getSignUpAndSignInErrorCount = ( return signUpErrorCount + signInMethodErrorCount; }; -export const getOthersErrorCount = ( - errors: FieldErrorsImpl> -) => { - const { termsOfUse } = errors; - - const termsOfUseErrorCount = termsOfUse ? Object.keys(termsOfUse).length : 0; - - return termsOfUseErrorCount; -}; +export const getOthersErrorCount = (errors: FieldErrorsImpl>) => + errors.termsOfUseUrl ? 1 : 0; diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index 6b9a702a4..f464aba13 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -2,13 +2,48 @@ import type { Branding, LanguageInfo, SignInExperience, - TermsOfUse, Color, SignUp, SignIn, } from '@logto/schemas'; import { BrandingStyle, SignInMode, SignInIdentifier } from '@logto/schemas'; +export const mockColor: Color = { + primaryColor: '#000', + isDarkModeEnabled: true, + darkPrimaryColor: '#fff', +}; + +export const mockBranding: Branding = { + style: BrandingStyle.Logo_Slogan, + logoUrl: 'http://silverhand.png', + slogan: 'Silverhand.', +}; + +export const mockTermsOfUseUrl = 'http://silverhand.com/terms'; + +export const mockLanguageInfo: LanguageInfo = { + autoDetect: true, + fallbackLanguage: 'en', +}; + +export const mockSignUp: SignUp = { + identifiers: [SignInIdentifier.Username], + password: true, + verify: false, +}; + +export const mockSignInMethod: SignIn['methods'][0] = { + identifier: SignInIdentifier.Username, + password: true, + verificationCode: false, + isPasswordPrimary: true, +}; + +export const mockSignIn = { + methods: [mockSignInMethod], +}; + export const mockSignInExperience: SignInExperience = { id: 'foo', color: { @@ -21,9 +56,7 @@ export const mockSignInExperience: SignInExperience = { logoUrl: 'http://logto.png', slogan: 'logto', }, - termsOfUse: { - enabled: false, - }, + termsOfUseUrl: mockTermsOfUseUrl, languageInfo: { autoDetect: true, fallbackLanguage: 'en', @@ -58,42 +91,3 @@ export const mockSignInExperience: SignInExperience = { socialSignInConnectorTargets: ['github', 'facebook', 'wechat'], signInMode: SignInMode.SignInAndRegister, }; - -export const mockColor: Color = { - primaryColor: '#000', - isDarkModeEnabled: true, - darkPrimaryColor: '#fff', -}; - -export const mockBranding: Branding = { - style: BrandingStyle.Logo_Slogan, - logoUrl: 'http://silverhand.png', - slogan: 'Silverhand.', -}; - -export const mockTermsOfUse: TermsOfUse = { - enabled: true, - contentUrl: 'http://silverhand.com/terms', -}; - -export const mockLanguageInfo: LanguageInfo = { - autoDetect: true, - fallbackLanguage: 'en', -}; - -export const mockSignUp: SignUp = { - identifiers: [SignInIdentifier.Username], - password: true, - verify: false, -}; - -export const mockSignInMethod: SignIn['methods'][0] = { - identifier: SignInIdentifier.Username, - password: true, - verificationCode: false, - isPasswordPrimary: true, -}; - -export const mockSignIn = { - methods: [mockSignInMethod], -}; 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 b41930a41..42a5f95e3 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -35,12 +35,8 @@ const { findDefaultSignInExperience, updateDefaultSignInExperience } = mockEsm( }) ); -const { - validateBranding, - validateTermsOfUse, - validateLanguageInfo, - removeUnavailableSocialConnectorTargets, -} = await import('./index.js'); +const { validateBranding, validateLanguageInfo, removeUnavailableSocialConnectorTargets } = + await import('./index.js'); beforeEach(() => { jest.clearAllMocks(); @@ -139,16 +135,6 @@ describe('validate language info', () => { }); }); -describe('validate terms of use', () => { - test('should throw when terms of use is enabled and content URL is empty', () => { - expect(() => { - validateTermsOfUse({ - enabled: true, - }); - }).toMatchError(new RequestError('sign_in_experiences.empty_content_url_of_terms_of_use')); - }); -}); - describe('remove unavailable social connector targets', () => { test('should remove unavailable social connector targets in sign-in experience', async () => { const mockSocialConnectorTargets = mockSocialConnectors.map( diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 2f81eb6c3..76e925819 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -1,5 +1,5 @@ import { builtInLanguages } from '@logto/phrases-ui'; -import type { Branding, LanguageInfo, SignInExperience, TermsOfUse } from '@logto/schemas'; +import type { Branding, LanguageInfo, SignInExperience } from '@logto/schemas'; import { SignInMode, ConnectorType, BrandingStyle } from '@logto/schemas'; import { adminConsoleApplicationId, @@ -42,13 +42,6 @@ export const validateLanguageInfo = async (languageInfo: LanguageInfo) => { ); }; -export const validateTermsOfUse = (termsOfUse: TermsOfUse) => { - assertThat( - !termsOfUse.enabled || termsOfUse.contentUrl, - 'sign_in_experiences.empty_content_url_of_terms_of_use' - ); -}; - export const removeUnavailableSocialConnectorTargets = async () => { const connectors = await getLogtoConnectors(); const availableSocialConnectorTargets = deduplicate( @@ -78,6 +71,7 @@ export const getSignInExperienceForApplication = async ( ...adminConsoleSignInExperience.branding, slogan: i18next.t('admin_console.welcome.title'), }, + termsOfUseUrl: signInExperience.termsOfUseUrl, languageInfo: signInExperience.languageInfo, signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register, socialSignInConnectorTargets: [], diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index f2977c95a..ec1319a4c 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -28,7 +28,7 @@ describe('sign-in-experience query', () => { ...mockSignInExperience, color: JSON.stringify(mockSignInExperience.color), branding: JSON.stringify(mockSignInExperience.branding), - termsOfUse: JSON.stringify(mockSignInExperience.termsOfUse), + termsOfUseUrl: mockSignInExperience.termsOfUseUrl, languageInfo: JSON.stringify(mockSignInExperience.languageInfo), signIn: JSON.stringify(mockSignInExperience.signIn), signUp: JSON.stringify(mockSignInExperience.signUp), @@ -38,7 +38,7 @@ describe('sign-in-experience query', () => { it('findDefaultSignInExperience', async () => { /* eslint-disable sql/no-unsafe-query */ const expectSql = ` - select "id", "color", "branding", "language_info", "terms_of_use", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode" + select "id", "color", "branding", "language_info", "terms_of_use_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode" from "sign_in_experiences" where "id"=$1 `; @@ -55,14 +55,12 @@ describe('sign-in-experience query', () => { }); it('updateDefaultSignInExperience', async () => { - const termsOfUse = { - enabled: false, - }; + const { termsOfUseUrl } = mockSignInExperience; /* eslint-disable sql/no-unsafe-query */ const expectSql = ` update "sign_in_experiences" - set "terms_of_use"=$1 + set "terms_of_use_url"=$1 where "id"=$2 returning * `; @@ -70,11 +68,11 @@ describe('sign-in-experience query', () => { mockQuery.mockImplementationOnce(async (sql, values) => { expectSqlAssert(sql, expectSql); - expect(values).toEqual([JSON.stringify(termsOfUse), id]); + expect(values).toEqual([termsOfUseUrl, id]); return createMockQueryResult([dbvalue]); }); - await expect(updateDefaultSignInExperience({ termsOfUse })).resolves.toEqual(dbvalue); + await expect(updateDefaultSignInExperience({ termsOfUseUrl })).resolves.toEqual(dbvalue); }); }); diff --git a/packages/core/src/routes/sign-in-experience.guard.test.ts b/packages/core/src/routes/sign-in-experience.guard.test.ts index 6f8f60466..2d99bf5e0 100644 --- a/packages/core/src/routes/sign-in-experience.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience.guard.test.ts @@ -9,7 +9,6 @@ import { mockGoogleConnector, mockLanguageInfo, mockSignInExperience, - mockTermsOfUse, } from '#src/__mocks__/index.js'; const { jest } = import.meta; @@ -59,44 +58,20 @@ beforeEach(() => { jest.clearAllMocks(); }); -describe('terms of use', () => { - describe('enabled', () => { - test.each(validBooleans)('%p should success', async (enabled) => { - const signInExperience = { termsOfUse: { ...mockTermsOfUse, enabled } }; - await expectPatchResponseStatus(signInExperience, 200); - }); - - test.each(invalidBooleans)('%p should fail', async (enabled) => { - const signInExperience = { termsOfUse: { ...mockTermsOfUse, enabled } }; - await expectPatchResponseStatus(signInExperience, 400); - }); - }); - - describe('contentUrl', () => { - test.each([undefined, 'http://silverhand.com/terms', 'https://logto.dev/terms'])( +describe('terms of use url', () => { + describe('termsOfUseUrl', () => { + test.each([undefined, null, '', 'http://silverhand.com/terms', 'https://logto.dev/terms'])( '%p should success', - async (contentUrl) => { - const signInExperience = { termsOfUse: { ...mockTermsOfUse, enabled: false, contentUrl } }; + async (termsOfUseUrl) => { + const signInExperience = { + termsOfUseUrl, + }; await expectPatchResponseStatus(signInExperience, 200); } ); - test.each([null, ' \t\n\r', 'non-url'])('%p should fail', async (contentUrl) => { - const signInExperience = { termsOfUse: { ...mockTermsOfUse, enabled: false, contentUrl } }; - await expectPatchResponseStatus(signInExperience, 400); - }); - - test('should allow empty contentUrl if termsOfUse is disabled', async () => { - const signInExperience = { - termsOfUse: { ...mockTermsOfUse, enabled: false, contentUrl: '' }, - }; - await expectPatchResponseStatus(signInExperience, 200); - }); - - test('should not allow empty contentUrl if termsOfUse is enabled', async () => { - const signInExperience = { - termsOfUse: { ...mockTermsOfUse, enabled: true, contentUrl: '' }, - }; + test.each([' \t\n\r', 'non-url'])('%p should fail', async (termsOfUseUrl) => { + const signInExperience = { termsOfUseUrl }; await expectPatchResponseStatus(signInExperience, 400); }); }); diff --git a/packages/core/src/routes/sign-in-experience.test.ts b/packages/core/src/routes/sign-in-experience.test.ts index d684e85e1..03c63523f 100644 --- a/packages/core/src/routes/sign-in-experience.test.ts +++ b/packages/core/src/routes/sign-in-experience.test.ts @@ -1,4 +1,4 @@ -import type { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas'; +import type { SignInExperience, CreateSignInExperience } from '@logto/schemas'; import { mockEsm, mockEsmWithActual, pickDefault } from '@logto/shared/esm'; import { @@ -13,24 +13,19 @@ import { mockSignIn, mockLanguageInfo, mockAliyunSmsConnector, + mockTermsOfUseUrl, } from '#src/__mocks__/index.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; -const { - validateBranding, - validateLanguageInfo, - validateTermsOfUse, - validateSignIn, - validateSignUp, -} = await mockEsmWithActual('#src/libraries/sign-in-experience/index.js', () => ({ - validateBranding: jest.fn(), - validateLanguageInfo: jest.fn(), - validateTermsOfUse: jest.fn(), - validateSignIn: jest.fn(), - validateSignUp: jest.fn(), -})); +const { validateBranding, validateLanguageInfo, validateSignIn, validateSignUp } = + await mockEsmWithActual('#src/libraries/sign-in-experience/index.js', () => ({ + validateBranding: jest.fn(), + validateLanguageInfo: jest.fn(), + validateSignIn: jest.fn(), + validateSignUp: jest.fn(), + })); const logtoConnectors = [ mockFacebookConnector, @@ -106,14 +101,13 @@ describe('PATCH /sign-in-exp', () => { }); it('should succeed to update when the input is valid', async () => { - const termsOfUse: TermsOfUse = { enabled: false }; const socialSignInConnectorTargets = ['github', 'facebook', 'wechat']; const response = await signInExperienceRequester.patch('/sign-in-exp').send({ color: mockColor, branding: mockBranding, languageInfo: mockLanguageInfo, - termsOfUse, + termsOfUseUrl: mockTermsOfUseUrl, socialSignInConnectorTargets, signUp: mockSignUp, signIn: mockSignIn, @@ -121,7 +115,6 @@ describe('PATCH /sign-in-exp', () => { expect(validateBranding).toHaveBeenCalledWith(mockBranding); expect(validateLanguageInfo).toHaveBeenCalledWith(mockLanguageInfo); - expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse); expect(validateSignUp).toHaveBeenCalledWith(mockSignUp, logtoConnectors); expect(validateSignIn).toHaveBeenCalledWith(mockSignIn, mockSignUp, logtoConnectors); @@ -131,7 +124,7 @@ describe('PATCH /sign-in-exp', () => { ...mockSignInExperience, color: mockColor, branding: mockBranding, - termsOfUse, + termsOfUseUrl: mockTermsOfUseUrl, socialSignInConnectorTargets, signIn: mockSignIn, }, diff --git a/packages/core/src/routes/sign-in-experience.ts b/packages/core/src/routes/sign-in-experience.ts index 944dd4605..ea7b421c5 100644 --- a/packages/core/src/routes/sign-in-experience.ts +++ b/packages/core/src/routes/sign-in-experience.ts @@ -1,10 +1,10 @@ import { ConnectorType, SignInExperiences } from '@logto/schemas'; +import { literal, object, string } from 'zod'; import { getLogtoConnectors } from '#src/connectors/index.js'; import { validateBranding, validateLanguageInfo, - validateTermsOfUse, validateSignUp, validateSignIn, } from '#src/libraries/sign-in-experience/index.js'; @@ -30,11 +30,18 @@ export default function signInExperiencesRoutes(router: router.patch( '/sign-in-exp', koaGuard({ - body: SignInExperiences.createGuard.omit({ id: true }).partial(), + body: SignInExperiences.createGuard + .omit({ id: true, termsOfUseUrl: true }) + .merge( + object({ + termsOfUseUrl: string().url().optional().nullable().or(literal('')), + }) + ) + .partial(), }), async (ctx, next) => { const { socialSignInConnectorTargets, ...rest } = ctx.guard.body; - const { branding, languageInfo, termsOfUse, signUp, signIn } = rest; + const { branding, languageInfo, signUp, signIn } = rest; if (branding) { validateBranding(branding); @@ -44,10 +51,6 @@ export default function signInExperiencesRoutes(router: await validateLanguageInfo(languageInfo); } - if (termsOfUse) { - validateTermsOfUse(termsOfUse); - } - const connectors = await getLogtoConnectors(); // Remove unavailable connectors diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 33c66db9d..740f6115c 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -108,6 +108,7 @@ describe('GET /.well-known/sign-in-exp', () => { ...adminConsoleSignInExperience.branding, slogan: 'admin_console.welcome.title', }, + termsOfUseUrl: mockSignInExperience.termsOfUseUrl, languageInfo: mockSignInExperience.languageInfo, socialConnectors: [], signInMode: SignInMode.SignIn, 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 2ba6e9025..69c9ba43e 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 @@ -22,10 +22,7 @@ describe('admin console sign-in experience', () => { logoUrl: 'https://logto.io/new-logo.png', darkLogoUrl: 'https://logto.io/new-dark-logo.png', }, - termsOfUse: { - enabled: true, - contentUrl: 'https://logto.io/terms', - }, + termsOfUseUrl: 'https://logto.io/terms', }; const updatedSignInExperience = await updateSignInExperience(newSignInExperience); 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 daa999123..a7ccca366 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 @@ -98,8 +98,6 @@ const sign_in_exp = { others: { terms_of_use: { title: 'NUTZUNGSBEDINGUNGEN', - enable: 'Aktiviere Nutzungsbedingungen', - description: 'Füge die rechtlichen Vereinbarungen für die Nutzung deines Produkts hinzu', terms_of_use: 'Nutzungsbedingungen', terms_of_use_placeholder: 'https://beispiel.de/nutzungsbedingungen', terms_of_use_tip: 'URL zu den Nutzungsbedingungen', 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 0f35ae8a3..819f8ddbb 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 @@ -96,8 +96,6 @@ const sign_in_exp = { others: { terms_of_use: { title: 'TERMS OF USE', - enable: 'Enable terms of use', - description: 'Add the legal agreements for the use of your product', terms_of_use: 'Terms of use', terms_of_use_placeholder: 'https://your.terms.of.use/', terms_of_use_tip: 'Terms of use URL', 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 f1818c26c..633aee1ee 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 @@ -98,8 +98,6 @@ const sign_in_exp = { others: { terms_of_use: { title: "CONDITIONS D'UTILISATION", - enable: "Activer les conditions d'utilisation", - description: "Ajouter les accords juridiques pour l'utilisation de votre produit", terms_of_use: "Conditions d'utilisation", terms_of_use_placeholder: 'https://vos.conditions.utilisation/', terms_of_use_tip: "Conditions d'utilisation URL", 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 de1321d9c..4dcf4cc1e 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 @@ -93,8 +93,6 @@ const sign_in_exp = { others: { terms_of_use: { title: '이용 약관', - enable: '이용 약관 활성화', - description: '서비스 사용을 위한 이용 약관을 추가해보세요.', terms_of_use: '이용 약관', terms_of_use_placeholder: 'https://your.terms.of.use/', terms_of_use_tip: '이용 약관 URL', 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 416837d00..7b0fa2396 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 @@ -98,8 +98,6 @@ const sign_in_exp = { others: { terms_of_use: { title: 'TERMOS DE USO', - enable: 'Habilitar termos de uso', - description: 'Adicione os acordos legais para o uso do seu produto', terms_of_use: 'Termos de uso', terms_of_use_placeholder: 'https://your.terms.of.use/', terms_of_use_tip: 'URL dos termos de uso', 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 ebf9f708c..76ebed176 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 @@ -96,8 +96,6 @@ const sign_in_exp = { others: { terms_of_use: { title: 'TERMOS DE USO', - enable: 'Ativar termos de uso', - description: 'Adicione os termos legais para uso do seu produto', terms_of_use: 'Termos de uso', terms_of_use_placeholder: 'https://your.terms.of.use/', terms_of_use_tip: 'URL dos termos de uso', 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 a3854af28..ca50061db 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 @@ -97,8 +97,6 @@ const sign_in_exp = { others: { terms_of_use: { title: 'KULLANIM KOŞULLARI', - enable: 'Kullanım koşullarını etkinleştir', - description: 'Ürününüzün kullanımına ilişkin yasal anlaşmaları ekleyin', terms_of_use: 'Kullanım koşulları', terms_of_use_placeholder: 'https://your.terms.of.use/', terms_of_use_tip: 'Kullanım koşulları URLi', 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 69bdb8cce..92c5ee972 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 @@ -89,8 +89,6 @@ const sign_in_exp = { others: { terms_of_use: { title: '使用条款', - enable: '开启使用条款', - description: '添加使用产品的法律协议。', terms_of_use: '使用条款', terms_of_use_placeholder: 'https://your.terms.of.use/', terms_of_use_tip: '使用条款 URL', diff --git a/packages/schemas/alterations/next-1671080370-terms-of-use.ts b/packages/schemas/alterations/next-1671080370-terms-of-use.ts new file mode 100644 index 000000000..4f05d31f0 --- /dev/null +++ b/packages/schemas/alterations/next-1671080370-terms-of-use.ts @@ -0,0 +1,86 @@ +import type { DatabaseTransactionConnection } from 'slonik'; +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +type DeprecatedTermsOfUse = { + enabled: boolean; + contentUrl?: string; +}; + +type DeprecatedSignInExperience = { + id: string; + termsOfUse: DeprecatedTermsOfUse; +}; + +type SignInExperience = { + id: string; + termsOfUseUrl?: string | null; +}; + +const alterTermsOfUse = async ( + signInExperience: DeprecatedSignInExperience, + pool: DatabaseTransactionConnection +) => { + const { + id, + termsOfUse: { enabled, contentUrl }, + } = signInExperience; + + if (enabled && contentUrl) { + await pool.query( + sql`update sign_in_experiences set terms_of_use_url = ${contentUrl} where id = ${id}` + ); + } +}; + +const rollbackTermsOfUse = async ( + signInExperience: SignInExperience, + pool: DatabaseTransactionConnection +) => { + const { id, termsOfUseUrl } = signInExperience; + + const termsOfUse: DeprecatedTermsOfUse = { + enabled: Boolean(termsOfUseUrl), + contentUrl: termsOfUseUrl ?? '', + }; + + await pool.query( + sql`update sign_in_experiences set terms_of_use = ${JSON.stringify( + termsOfUse + )} where id = ${id}` + ); +}; + +const alteration: AlterationScript = { + up: async (pool) => { + const rows = await pool.many( + sql`select * from sign_in_experiences` + ); + + await pool.query(sql` + alter table sign_in_experiences add column terms_of_use_url varchar(2048) + `); + + await Promise.all(rows.map(async (row) => alterTermsOfUse(row, pool))); + + await pool.query(sql` + alter table sign_in_experiences drop column terms_of_use + `); + }, + down: async (pool) => { + const rows = await pool.many(sql`select * from sign_in_experiences`); + + await pool.query(sql` + alter table sign_in_experiences add column terms_of_use jsonb not null default '{}'::jsonb + `); + + await Promise.all(rows.map(async (row) => rollbackTermsOfUse(row, pool))); + + await pool.query(sql` + alter table sign_in_experiences drop column terms_of_use_url + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 224e32578..3d1520ea3 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -119,13 +119,6 @@ export const brandingGuard = z.object({ export type Branding = z.infer; -export const termsOfUseGuard = z.object({ - enabled: z.boolean(), - contentUrl: z.string().url().optional().or(z.literal('')), -}); - -export type TermsOfUse = z.infer; - export const languageInfoGuard = z.object({ autoDetect: z.boolean(), fallbackLanguage: languageTagGuard, diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index b5123ff64..4ac4a7345 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -22,9 +22,7 @@ export const defaultSignInExperience: Readonly = { autoDetect: true, fallbackLanguage: 'en', }, - termsOfUse: { - enabled: false, - }, + termsOfUseUrl: null, signUp: { identifiers: [SignInIdentifier.Username], password: true, diff --git a/packages/schemas/tables/sign_in_experiences.sql b/packages/schemas/tables/sign_in_experiences.sql index 3c6dc0827..c0f7ce154 100644 --- a/packages/schemas/tables/sign_in_experiences.sql +++ b/packages/schemas/tables/sign_in_experiences.sql @@ -5,7 +5,7 @@ create table sign_in_experiences ( color jsonb /* @use Color */ not null, branding jsonb /* @use Branding */ not null, language_info jsonb /* @use LanguageInfo */ not null, - terms_of_use jsonb /* @use TermsOfUse */ not null, + terms_of_use_url varchar(2048), sign_in jsonb /* @use SignIn */ not null, sign_up jsonb /* @use SignUp */ not null, social_sign_in_connector_targets jsonb /* @use ConnectorTargets */ not null default '[]'::jsonb, diff --git a/packages/ui/src/__mocks__/logto.tsx b/packages/ui/src/__mocks__/logto.tsx index 3ddb84131..fbae28a93 100644 --- a/packages/ui/src/__mocks__/logto.tsx +++ b/packages/ui/src/__mocks__/logto.tsx @@ -192,10 +192,7 @@ export const mockSignInExperience: SignInExperience = { logoUrl: 'http://logto.png', slogan: 'logto', }, - termsOfUse: { - enabled: true, - contentUrl: 'http://terms.of.use/', - }, + termsOfUseUrl: 'http://terms.of.use/', languageInfo: { autoDetect: true, fallbackLanguage: 'en', @@ -216,7 +213,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = { id: mockSignInExperience.id, color: mockSignInExperience.color, branding: mockSignInExperience.branding, - termsOfUse: mockSignInExperience.termsOfUse, + termsOfUseUrl: mockSignInExperience.termsOfUseUrl, languageInfo: mockSignInExperience.languageInfo, signIn: mockSignInExperience.signIn, signUp: { diff --git a/packages/ui/src/containers/TermsOfUse/TermsOfUseConfirmModalContent/index.tsx b/packages/ui/src/containers/TermsOfUse/TermsOfUseConfirmModalContent/index.tsx index 2ad598400..7170e9237 100644 --- a/packages/ui/src/containers/TermsOfUse/TermsOfUseConfirmModalContent/index.tsx +++ b/packages/ui/src/containers/TermsOfUse/TermsOfUseConfirmModalContent/index.tsx @@ -2,6 +2,7 @@ import { useContext } from 'react'; import { useTranslation, Trans } from 'react-i18next'; import TextLink from '@/components/TextLink'; +import type { Props as TextLinkProps } from '@/components/TextLink'; import type { ModalContentRenderProps } from '@/hooks/use-confirm-modal'; import { PageContext } from '@/hooks/use-page-context'; import usePlatform from '@/hooks/use-platform'; @@ -9,19 +10,19 @@ import { ConfirmModalMessage } from '@/types'; const TermsOfUseConfirmModalContent = ({ cancel }: ModalContentRenderProps) => { const { experienceSettings } = useContext(PageContext); - const { termsOfUse } = experienceSettings ?? {}; + const { termsOfUseUrl } = experienceSettings ?? {}; const { t } = useTranslation(); const { isMobile } = usePlatform(); - const linkProps = isMobile + const linkProps: TextLinkProps = isMobile ? { onClick: () => { cancel(ConfirmModalMessage.SHOW_TERMS_DETAIL_MODAL); }, } : { - href: termsOfUse?.contentUrl, + href: termsOfUseUrl ?? undefined, target: '_blank', }; diff --git a/packages/ui/src/containers/TermsOfUse/index.tsx b/packages/ui/src/containers/TermsOfUse/index.tsx index 82ac87227..e43093f4d 100644 --- a/packages/ui/src/containers/TermsOfUse/index.tsx +++ b/packages/ui/src/containers/TermsOfUse/index.tsx @@ -7,11 +7,11 @@ type Props = { }; const TermsOfUse = ({ className }: Props) => { - const { termsAgreement, setTermsAgreement, termsSettings, termsOfUseIframeModalHandler } = + const { termsAgreement, setTermsAgreement, termsOfUseUrl, termsOfUseIframeModalHandler } = useTerms(); const { isMobile } = usePlatform(); - if (!termsSettings?.enabled || !termsSettings.contentUrl) { + if (!termsOfUseUrl) { return null; } @@ -19,7 +19,7 @@ const TermsOfUse = ({ className }: Props) => { { setTermsAgreement(checked); diff --git a/packages/ui/src/hooks/use-terms.ts b/packages/ui/src/hooks/use-terms.ts index a51a4f4e6..603230720 100644 --- a/packages/ui/src/hooks/use-terms.ts +++ b/packages/ui/src/hooks/use-terms.ts @@ -12,12 +12,12 @@ const useTerms = () => { const { termsAgreement, setTermsAgreement, experienceSettings } = useContext(PageContext); const { show } = useConfirmModal(); - const { termsOfUse } = experienceSettings ?? {}; + const { termsOfUseUrl } = experienceSettings ?? {}; const termsOfUseIframeModalHandler = useCallback(async () => { const [result] = await show({ className: styles.iframeModal, - ModalContent: () => createIframeConfirmModalContent(termsOfUse?.contentUrl), + ModalContent: () => createIframeConfirmModalContent(termsOfUseUrl ?? undefined), confirmText: 'action.agree', }); @@ -27,7 +27,7 @@ const useTerms = () => { } return result; - }, [setTermsAgreement, show, termsOfUse?.contentUrl]); + }, [setTermsAgreement, show, termsOfUseUrl]); const termsOfUseConfirmModalHandler = useCallback(async () => { const [result, data] = await show({ @@ -51,15 +51,15 @@ const useTerms = () => { }, [setTermsAgreement, show, termsOfUseIframeModalHandler]); const termsValidation = useCallback(async () => { - if (termsAgreement || !termsOfUse?.enabled || !termsOfUse.contentUrl) { + if (termsAgreement || !termsOfUseUrl) { return true; } return termsOfUseConfirmModalHandler(); - }, [termsAgreement, termsOfUse, termsOfUseConfirmModalHandler]); + }, [termsAgreement, termsOfUseUrl, termsOfUseConfirmModalHandler]); return { - termsSettings: termsOfUse, + termsOfUseUrl, termsAgreement, termsValidation, setTermsAgreement, diff --git a/packages/ui/src/utils/sign-in-experience.test.ts b/packages/ui/src/utils/sign-in-experience.test.ts index 8c7be2dc3..37887ebaa 100644 --- a/packages/ui/src/utils/sign-in-experience.test.ts +++ b/packages/ui/src/utils/sign-in-experience.test.ts @@ -16,7 +16,7 @@ describe('getSignInExperienceSettings', () => { expect(settings.branding).toEqual(mockSignInExperience.branding); expect(settings.languageInfo).toEqual(mockSignInExperience.languageInfo); - expect(settings.termsOfUse).toEqual(mockSignInExperience.termsOfUse); + expect(settings.termsOfUseUrl).toEqual(mockSignInExperience.termsOfUseUrl); expect(settings.signUp.identifiers).toContain('username'); expect(settings.signIn.methods).toHaveLength(3); });