diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 727cc96eb0..863de44485 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -355,6 +355,7 @@
"submit": "Create account",
"creating": "Creating account...",
"alreadyHaveAccount": "Already have an account?",
+ "legal": "By creating an account you agree to the Terms and Privacy policy.",
"errors": {
"failed": "Registration failed."
},
@@ -1039,6 +1040,8 @@
"twitter": "X / Twitter",
"legal": "Legal",
"license": "License (Apache-2.0)",
+ "terms": "Terms",
+ "privacy": "Privacy",
"copyright": "© {{year}} Commonly. Code licensed under Apache-2.0; the Commonly name and logo are trademarks of the Commonly project."
}
},
diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json
index e9260fe305..eca298ed86 100644
--- a/frontend/src/i18n/locales/zh-CN.json
+++ b/frontend/src/i18n/locales/zh-CN.json
@@ -355,6 +355,7 @@
"submit": "注册",
"creating": "正在注册…",
"alreadyHaveAccount": "已有账号?",
+ "legal": "创建账号即表示你同意服务条款和隐私政策。",
"errors": {
"failed": "注册失败。"
},
@@ -1038,6 +1039,8 @@
"twitter": "X / Twitter",
"legal": "法律信息",
"license": "许可证(Apache-2.0)",
+ "terms": "服务条款",
+ "privacy": "隐私政策",
"copyright": "© {{year}} Commonly。代码采用 Apache-2.0 许可;Commonly 名称和标志是 Commonly 项目的商标。"
}
},
diff --git a/frontend/src/v2/__tests__/V2Register.test.tsx b/frontend/src/v2/__tests__/V2Register.test.tsx
index 6c303f46d1..b69b22cc9a 100644
--- a/frontend/src/v2/__tests__/V2Register.test.tsx
+++ b/frontend/src/v2/__tests__/V2Register.test.tsx
@@ -316,3 +316,16 @@ describe('V2Register invite code handoff', () => {
expect(body.invitationCode).toBe('CM-KEPT');
});
});
+
+// Launch row 2: signup links the Terms and Privacy pages it asks people to agree to.
+describe('V2Register legal links', () => {
+ beforeEach(() => {
+ axios.get.mockResolvedValue({ data: { inviteOnly: false, providers: [] } });
+ });
+
+ it('links Terms and Privacy under the form', async () => {
+ renderRegister();
+ expect(await screen.findByRole('link', { name: 'Terms' })).toHaveAttribute('href', '/terms');
+ expect(screen.getByRole('link', { name: 'Privacy' })).toHaveAttribute('href', '/privacy');
+ });
+});
diff --git a/frontend/src/v2/components/V2Register.tsx b/frontend/src/v2/components/V2Register.tsx
index ba14429a24..9a0918b69f 100644
--- a/frontend/src/v2/components/V2Register.tsx
+++ b/frontend/src/v2/components/V2Register.tsx
@@ -239,6 +239,16 @@ const V2Register: React.FC = () => {
+
+ ,
+ privacy: ,
+ }}
+ />
+
+
{t('auth.register.alreadyHaveAccount')}
{' '}
diff --git a/frontend/src/v2/landing/V2LandingPage.tsx b/frontend/src/v2/landing/V2LandingPage.tsx
index eaa8b11ea1..280d4a4967 100644
--- a/frontend/src/v2/landing/V2LandingPage.tsx
+++ b/frontend/src/v2/landing/V2LandingPage.tsx
@@ -965,6 +965,8 @@ const V2LandingPage: React.FC = () => {
{/* Copyright is retained under Apache-2.0 — the license grants
diff --git a/frontend/src/v2/landing/__tests__/landingLegalLinks.test.tsx b/frontend/src/v2/landing/__tests__/landingLegalLinks.test.tsx
new file mode 100644
index 0000000000..9399821292
--- /dev/null
+++ b/frontend/src/v2/landing/__tests__/landingLegalLinks.test.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { render, screen, within } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import axios from 'axios';
+import i18n from '../../../i18n';
+import { useAuth } from '../../../context/AuthContext';
+import V2LandingPage from '../V2LandingPage';
+
+/**
+ * Launch row 2. /terms and /privacy shipped with no link reaching them; the
+ * footer's Legal column is where a visitor looks for both.
+ */
+
+jest.mock('axios');
+jest.mock('../../../context/AuthContext', () => ({
+ useAuth: jest.fn(),
+}));
+
+describe('V2LandingPage legal links', () => {
+ beforeEach(async () => {
+ (axios.get as jest.Mock).mockResolvedValue({ data: {} });
+ (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: false });
+ await i18n.changeLanguage('en');
+ });
+
+ afterEach(() => jest.clearAllMocks());
+
+ it('links Terms and Privacy from the footer', () => {
+ const { container } = render();
+ const footer = container.querySelector('footer') as HTMLElement;
+ expect(within(footer).getByRole('link', { name: 'Terms' })).toHaveAttribute('href', '/terms');
+ expect(within(footer).getByRole('link', { name: 'Privacy' })).toHaveAttribute('href', '/privacy');
+ });
+});