diff --git a/.changeset-staged/great-turkeys-fry.md b/.changeset-staged/great-turkeys-fry.md
new file mode 100644
index 000000000..699a4a5c9
--- /dev/null
+++ b/.changeset-staged/great-turkeys-fry.md
@@ -0,0 +1,10 @@
+---
+"@logto/console": minor
+"@logto/core": minor
+"@logto/phrases": minor
+"@logto/schemas": minor
+---
+
+### Add privacy policy url
+
+In addition to the terms of service url, we also provide a privacy policy url field in the sign-in-experience settings. To better support the end-users' privacy declaration needs.
diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx
index 2eb217050..13ac07712 100644
--- a/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx
+++ b/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx
@@ -32,6 +32,19 @@ const TermsForm = () => {
placeholder={t('sign_in_exp.others.terms_of_use.terms_of_use_placeholder')}
/>
+
+ !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.privacy_policy_placeholder')}
+ />
+
);
};
diff --git a/packages/console/src/pages/SignInExperience/utils/form.ts b/packages/console/src/pages/SignInExperience/utils/form.ts
index a6b992b0c..726e635d6 100644
--- a/packages/console/src/pages/SignInExperience/utils/form.ts
+++ b/packages/console/src/pages/SignInExperience/utils/form.ts
@@ -114,5 +114,11 @@ export const getSignUpAndSignInErrorCount = (
return signUpErrorCount + signInMethodErrorCount;
};
-export const getOthersErrorCount = (errors: FieldErrorsImpl>) =>
- errors.termsOfUseUrl ? 1 : 0;
+export const getOthersErrorCount = (
+ errors: FieldErrorsImpl>
+) => {
+ const termsOfUseUrlErrorCount = errors.termsOfUseUrl ? 1 : 0;
+ const privacyPolicyUrlErrorCount = errors.privacyPolicyUrl ? 1 : 0;
+
+ return termsOfUseUrlErrorCount + privacyPolicyUrlErrorCount;
+};
diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts
index 2a4ce1b1a..1373e93a8 100644
--- a/packages/core/src/__mocks__/sign-in-experience.ts
+++ b/packages/core/src/__mocks__/sign-in-experience.ts
@@ -21,6 +21,7 @@ export const mockBranding: Branding = {
};
export const mockTermsOfUseUrl = 'http://silverhand.com/terms';
+export const mockPrivacyPolicyUrl = 'http://silverhand.com/privacy';
export const mockLanguageInfo: LanguageInfo = {
autoDetect: true,
@@ -58,6 +59,7 @@ export const mockSignInExperience: SignInExperience = {
slogan: 'logto',
},
termsOfUseUrl: mockTermsOfUseUrl,
+ privacyPolicyUrl: mockPrivacyPolicyUrl,
languageInfo: {
autoDetect: true,
fallbackLanguage: 'en',
diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts
index 0fde93b2d..5ffc6ed03 100644
--- a/packages/core/src/queries/sign-in-experience.test.ts
+++ b/packages/core/src/queries/sign-in-experience.test.ts
@@ -35,7 +35,7 @@ describe('sign-in-experience query', () => {
it('findDefaultSignInExperience', async () => {
/* eslint-disable sql/no-unsafe-query */
const expectSql = `
- select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css"
+ select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css"
from "sign_in_experiences"
where "id"=$1
`;
diff --git a/packages/core/src/routes/sign-in-experience/guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.test.ts
index 56e4ffc3b..b5c29cf67 100644
--- a/packages/core/src/routes/sign-in-experience/guard.test.ts
+++ b/packages/core/src/routes/sign-in-experience/guard.test.ts
@@ -68,6 +68,25 @@ describe('terms of use url', () => {
});
});
+describe('privacy policy url', () => {
+ describe('privacyPolicyUrl', () => {
+ test.each([undefined, null, '', 'http://silverhand.com/privacy', 'https://logto.dev/privacy'])(
+ '%p should success',
+ async (privacyPolicyUrl) => {
+ const signInExperience = {
+ privacyPolicyUrl,
+ };
+ await expectPatchResponseStatus(signInExperience, 200);
+ }
+ );
+
+ test.each([' \t\n\r', 'non-url'])('%p should fail', async (privacyPolicyUrl) => {
+ const signInExperience = { privacyPolicyUrl };
+ await expectPatchResponseStatus(signInExperience, 400);
+ });
+ });
+});
+
describe('languageInfo', () => {
describe('autoDetect', () => {
test.each(validBooleans)('%p should success', async (autoDetect) => {
diff --git a/packages/core/src/routes/sign-in-experience/index.test.ts b/packages/core/src/routes/sign-in-experience/index.test.ts
index 2c6b02eaf..46711da2a 100644
--- a/packages/core/src/routes/sign-in-experience/index.test.ts
+++ b/packages/core/src/routes/sign-in-experience/index.test.ts
@@ -14,6 +14,7 @@ import {
mockLanguageInfo,
mockAliyunSmsConnector,
mockTermsOfUseUrl,
+ mockPrivacyPolicyUrl,
} from '#src/__mocks__/index.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createRequester } from '#src/utils/test-utils.js';
@@ -118,6 +119,7 @@ describe('PATCH /sign-in-exp', () => {
branding: mockBranding,
languageInfo: mockLanguageInfo,
termsOfUseUrl: mockTermsOfUseUrl,
+ privacyPolicyUrl: mockPrivacyPolicyUrl,
socialSignInConnectorTargets,
signUp: mockSignUp,
signIn: mockSignIn,
@@ -135,6 +137,7 @@ describe('PATCH /sign-in-exp', () => {
color: mockColor,
branding: mockBranding,
termsOfUseUrl: mockTermsOfUseUrl,
+ privacyPolicyUrl: mockPrivacyPolicyUrl,
socialSignInConnectorTargets,
signIn: mockSignIn,
},
diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts
index 840639e30..45a6e0e02 100644
--- a/packages/core/src/routes/sign-in-experience/index.ts
+++ b/packages/core/src/routes/sign-in-experience/index.ts
@@ -33,10 +33,11 @@ export default function signInExperiencesRoutes(
'/sign-in-exp',
koaGuard({
body: SignInExperiences.createGuard
- .omit({ id: true, termsOfUseUrl: true })
+ .omit({ id: true, termsOfUseUrl: true, privacyPolicyUrl: true })
.merge(
object({
termsOfUseUrl: string().url().optional().nullable().or(literal('')),
+ privacyPolicyUrl: string().url().optional().nullable().or(literal('')),
})
)
.partial(),
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 69c9ba43e..fc5127b9c 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
@@ -23,6 +23,7 @@ describe('admin console sign-in experience', () => {
darkLogoUrl: 'https://logto.io/new-dark-logo.png',
},
termsOfUseUrl: 'https://logto.io/terms',
+ privacyPolicyUrl: 'https://logto.io/privacy',
};
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 228c74fe6..c2a185485 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
@@ -102,6 +102,9 @@ const sign_in_exp = {
terms_of_use: 'Nutzungsbedingungen',
terms_of_use_placeholder: 'https://beispiel.de/nutzungsbedingungen',
terms_of_use_tip: 'URL zu den Nutzungsbedingungen',
+ privacy_policy: 'Datenschutzrichtlinien',
+ privacy_policy_placeholder: 'https://beispiel.de/datenschutzrichtlinien',
+ privacy_policy_tip: 'URL zu den Datenschutzrichtlinien',
},
languages: {
title: 'SPRACHEN',
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 09ddcd657..e636a07ca 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
@@ -100,6 +100,9 @@ const sign_in_exp = {
terms_of_use: 'Terms of use',
terms_of_use_placeholder: 'https://your.terms.of.use/',
terms_of_use_tip: 'Terms of use URL',
+ privacy_policy: 'Privacy policy',
+ privacy_policy_placeholder: 'https://your.privacy.policy/',
+ privacy_policy_tip: 'Privacy policy URL',
},
languages: {
title: 'LANGUAGES',
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 44bd7e6fe..f04984be2 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
@@ -102,6 +102,9 @@ const sign_in_exp = {
terms_of_use: "Conditions d'utilisation",
terms_of_use_placeholder: 'https://vos.conditions.utilisation/',
terms_of_use_tip: "Conditions d'utilisation URL",
+ privacy_policy: 'Politique de confidentialité',
+ privacy_policy_placeholder: 'https://votre.politique.confidentialite/',
+ privacy_policy_tip: 'Politique de confidentialité URL',
},
languages: {
title: 'LANGAGES',
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 88ef6c449..636340254 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
@@ -96,6 +96,9 @@ const sign_in_exp = {
terms_of_use: '이용 약관',
terms_of_use_placeholder: 'https://your.terms.of.use/',
terms_of_use_tip: '이용 약관 URL',
+ privacy_policy: '개인정보 처리방침',
+ privacy_policy_placeholder: 'https://your.privacy.policy/',
+ privacy_policy_tip: '개인정보 처리방침 URL',
},
languages: {
title: '언어',
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 a420e8c55..5e7acd14a 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
@@ -102,6 +102,9 @@ const sign_in_exp = {
terms_of_use: 'Termos de uso',
terms_of_use_placeholder: 'https://your.terms.of.use/',
terms_of_use_tip: 'URL dos termos de uso',
+ privacy_policy: 'Política de privacidade',
+ privacy_policy_placeholder: 'https://your.privacy.policy/',
+ privacy_policy_tip: 'URL da política de privacidade',
},
languages: {
title: 'IDIOMAS',
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 25c01fd09..82c456f32 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
@@ -100,6 +100,9 @@ const sign_in_exp = {
terms_of_use: 'Termos de uso',
terms_of_use_placeholder: 'https://your.terms.of.use/',
terms_of_use_tip: 'URL dos termos de uso',
+ privacy_policy: 'Política de privacidade',
+ privacy_policy_placeholder: 'https://your.privacy.policy/',
+ privacy_policy_tip: 'URL da política de privacidade',
},
languages: {
title: 'LÍNGUAS',
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 0f9a202ed..923b80926 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
@@ -101,6 +101,9 @@ const sign_in_exp = {
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',
+ privacy_policy: 'Gizlilik politikası',
+ privacy_policy_placeholder: 'https://your.privacy.policy/',
+ privacy_policy_tip: 'Gizlilik politikası URLi',
},
languages: {
title: 'DİLLER',
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 c9cf49fb7..07322bd80 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
@@ -94,6 +94,9 @@ const sign_in_exp = {
terms_of_use: '使用条款',
terms_of_use_placeholder: 'https://your.terms.of.use/',
terms_of_use_tip: '使用条款 URL',
+ privacy_policy: '隐私政策',
+ privacy_policy_placeholder: 'https://your.privacy.policy/',
+ privacy_policy_tip: '隐私政策 URL',
},
languages: {
title: '语言',
diff --git a/packages/schemas/alterations/next-1678157950-privacy-policy-url.ts b/packages/schemas/alterations/next-1678157950-privacy-policy-url.ts
new file mode 100644
index 000000000..cf6330b4b
--- /dev/null
+++ b/packages/schemas/alterations/next-1678157950-privacy-policy-url.ts
@@ -0,0 +1,20 @@
+import { sql } from 'slonik';
+
+import type { AlterationScript } from '../lib/types/alteration.js';
+
+const alteration: AlterationScript = {
+ up: async (pool) => {
+ await pool.query(sql`
+ alter table sign_in_experiences
+ add column if not exists privacy_policy_url varchar(2048);
+ `);
+ },
+ down: async (pool) => {
+ await pool.query(sql`
+ alter table sign_in_experiences
+ drop column privacy_policy_url;
+ `);
+ },
+};
+
+export default alteration;
diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts
index 2e7ba4136..6a8453a52 100644
--- a/packages/schemas/src/seeds/sign-in-experience.ts
+++ b/packages/schemas/src/seeds/sign-in-experience.ts
@@ -26,6 +26,7 @@ export const createDefaultSignInExperience = (forTenantId: string): Readonly