mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
improve privacy consent modal: policy link, clearer CTAs, mobile layout (#1921)
The first-run consent banner had no link to a privacy policy, an
affirmative button ("Help improve") that didn't read as a consent
choice, and a fixed bottom-right card that crowded content on phones.
- Add a "Read the privacy policy" link (external-link icon, accent,
underlined) above the actions, plus a root PRIVACY.md it points to
documenting the telemetry behaviour the modal discloses.
- Rename the CTAs to "Share usage data" / "Don't share" so both name
the action; they stay equal-prominence per the EDPB/GDPR comment.
- Stretch the banner to a bottom-edge bar under 540px with a
safe-area inset so it clears mobile browser chrome.
- Add PrivacyConsentModal tests; sync the new i18n key to every
locale and update the consent-label assertion in App.connectors.
Refs #1756
This commit is contained in:
parent
9cc1fb28f3
commit
30ad8b8ac3
25 changed files with 263 additions and 46 deletions
70
PRIVACY.md
Normal file
70
PRIVACY.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Privacy
|
||||
|
||||
This page describes what data the Open Design desktop and web app collects,
|
||||
when it collects it, and how you stay in control. It documents the behavior
|
||||
shipped in the app — the same controls live under **Settings → Privacy**.
|
||||
|
||||
Open Design is **local-first**. Your projects, generated files, and BYOK API
|
||||
keys stay on your machine. The app works fully offline; nothing in this page
|
||||
applies unless you explicitly turn telemetry on.
|
||||
|
||||
## Telemetry is opt-in
|
||||
|
||||
Usage telemetry is **off by default**. On first run the app shows a privacy
|
||||
consent banner asking you to make a choice — it never starts sending anything
|
||||
before you do. You can change your decision at any time under
|
||||
**Settings → Privacy**, where each category below has its own toggle.
|
||||
|
||||
## What is collected when you opt in
|
||||
|
||||
When telemetry is enabled, the app may send the following to the Open Design
|
||||
team. Each category is independently controllable in Settings.
|
||||
|
||||
- **Anonymous metrics** — run counts, token usage, error rate, and duration.
|
||||
No prompts and no project data.
|
||||
- **Conversation and tool content** — your prompts, assistant responses, tool
|
||||
inputs, and tool outputs (truncated before send). API keys, tokens, JWTs,
|
||||
emails, IP addresses, and credit-card numbers are stripped automatically
|
||||
before anything leaves your machine.
|
||||
- **Project artifacts manifest** — filenames, types, and sizes of generated
|
||||
files. The **contents** of those files are never sent.
|
||||
|
||||
## What is never collected
|
||||
|
||||
- The contents of your generated artifact files.
|
||||
- Your BYOK API keys, tokens, or other secrets — these are redacted before
|
||||
send and are never part of telemetry.
|
||||
- Anything at all while telemetry is turned off.
|
||||
|
||||
## How telemetry is sent
|
||||
|
||||
Redacted telemetry batches are sent to a Cloudflare Worker relay operated by
|
||||
the Open Design team, which forwards them to [Langfuse](https://langfuse.com)
|
||||
for analysis. The relay holds the Langfuse write credentials server-side, so
|
||||
packaged clients only ever ship a public relay URL — no secret keys. If the
|
||||
relay is unavailable the app retries quietly and keeps working; telemetry
|
||||
never blocks your workflow.
|
||||
|
||||
## Your anonymous ID
|
||||
|
||||
When telemetry is enabled the app generates a random, opaque installation ID
|
||||
so related events can be grouped. It is not tied to your name, email, or
|
||||
account, and it carries no personal information.
|
||||
|
||||
## Deleting your data
|
||||
|
||||
**Settings → Privacy → Delete my data** rotates your anonymous ID and stops
|
||||
sending. Telemetry already received ages out under the team's retention
|
||||
policy.
|
||||
|
||||
## Bring your own key
|
||||
|
||||
Open Design is BYOK at every layer. The API keys you configure for coding
|
||||
agents and model providers are stored locally and used only to talk to those
|
||||
providers directly. They are never sent to the Open Design team.
|
||||
|
||||
## Changes to this page
|
||||
|
||||
This document tracks the data handling of the shipped app. When the telemetry
|
||||
behavior changes, this page is updated alongside it. For questions, open a
|
||||
[GitHub Discussion](https://github.com/nexu-io/open-design/discussions).
|
||||
|
|
@ -1,4 +1,12 @@
|
|||
import { useT } from '../i18n';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
/**
|
||||
* Canonical location of the full privacy policy. Kept as a single named
|
||||
* constant so it can be repointed (e.g. to a hosted page) without touching
|
||||
* markup. `PRIVACY.md` documents the same data handling the modal discloses.
|
||||
*/
|
||||
const PRIVACY_POLICY_URL = 'https://github.com/nexu-io/open-design/blob/main/PRIVACY.md';
|
||||
|
||||
interface Props {
|
||||
/** Affirmative consent (Share usage data). */
|
||||
|
|
@ -12,11 +20,17 @@ interface Props {
|
|||
*
|
||||
* Anchored to the bottom-right of the viewport (cookie-consent style)
|
||||
* so it's prominently visible without blocking the underlying app —
|
||||
* the user can move around and read while deciding. The two action
|
||||
* buttons share equal visual prominence so the reject path is not
|
||||
* de-emphasised, matching the EDPB equal-prominence requirement
|
||||
* under GDPR. Neither button is rendered as selected before the user
|
||||
* chooses.
|
||||
* the user can move around and read while deciding. On narrow viewports
|
||||
* it stretches to a bottom-edge bar (see `.privacy-consent-banner` in
|
||||
* index.css) so it doesn't crowd content on phones.
|
||||
*
|
||||
* The two action buttons share equal visual prominence so the reject
|
||||
* path is not de-emphasised, matching the EDPB equal-prominence
|
||||
* requirement under GDPR. Neither button is rendered as selected before
|
||||
* the user chooses, and their labels name the action ("Share usage
|
||||
* data" / "Don't share") rather than a vague outcome so the affirmative
|
||||
* button reads as a consent choice. A link to the full privacy policy
|
||||
* sits above the actions so the policy is reachable before deciding.
|
||||
*
|
||||
* Stays mounted until the user picks Share or Don't share — there is
|
||||
* no dismiss-without-choice button on purpose. Telemetry decisions
|
||||
|
|
@ -47,6 +61,16 @@ export function PrivacyConsentModal({ onShare, onDecline }: Props): JSX.Element
|
|||
|
||||
<p className="hint privacy-consent-banner-footer">{t('settings.privacyConsentFooter')}</p>
|
||||
|
||||
<a
|
||||
className="privacy-consent-policy-link"
|
||||
href={PRIVACY_POLICY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="external-link" size={13} />
|
||||
<span>{t('settings.privacyConsentPolicyLink')}</span>
|
||||
</a>
|
||||
|
||||
<div
|
||||
className="privacy-consent-actions"
|
||||
role="group"
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const ar: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const de: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -181,8 +181,9 @@ export const en: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation and tool content',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const esES: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const fa: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const fr: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const hu: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -173,8 +173,9 @@ export const id: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -167,8 +167,9 @@ export const it: Dict = {
|
|||
'settings.privacyConsentKicker': 'Aiutaci a migliorare Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design può condividere dati di utilizzo con il nostro team per aiutarci a migliorare. Questo include:',
|
||||
'settings.privacyConsentFooter': 'Puoi modificare entrambe queste opzioni in qualsiasi momento in Impostazioni → Privacy. Non carichiamo mai i contenuti dei tuoi file di artefatti generati.',
|
||||
'settings.privacyConsentShare': 'Aiuta a migliorare',
|
||||
'settings.privacyConsentDecline': 'Non ora',
|
||||
'settings.privacyConsentShare': 'Condividi i dati di utilizzo',
|
||||
'settings.privacyConsentDecline': 'Non condividere',
|
||||
'settings.privacyConsentPolicyLink': "Leggi l'informativa sulla privacy",
|
||||
'settings.privacyMetrics': 'Metriche anonime',
|
||||
'settings.privacyMetricsHint': 'Conteggi di esecuzione, utilizzo di token, tasso di errore, durata. Nessun prompt, nessun dato di progetto.',
|
||||
'settings.privacyContent': 'Contenuto della conversazione',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const ja: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const ko: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -175,8 +175,9 @@ export const pl: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -174,8 +174,9 @@ export const ptBR: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -174,8 +174,9 @@ export const ru: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -158,8 +158,9 @@ export const th: Dict = {
|
|||
'settings.privacyConsentKicker': 'ช่วยเราพัฒนา Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design สามารถแชร์ข้อมูลการใช้งานกับทีมของเราเพื่อช่วยพัฒนา ซึ่งรวมถึง:',
|
||||
'settings.privacyConsentFooter': 'คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ตลอดเวลาใน การตั้งค่า → ความเป็นส่วนตัว เราจะไม่ส่งเนื้อหาในไฟล์ที่คุณสร้างขึ้น',
|
||||
'settings.privacyConsentShare': 'ช่วยปรับปรุง',
|
||||
'settings.privacyConsentDecline': 'ไว้ทีหลัง',
|
||||
'settings.privacyConsentShare': 'แชร์ข้อมูลการใช้งาน',
|
||||
'settings.privacyConsentDecline': 'ไม่แชร์',
|
||||
'settings.privacyConsentPolicyLink': 'อ่านนโยบายความเป็นส่วนตัว',
|
||||
'settings.privacyMetrics': 'ข้อมูลผู้ใช้นิรนาม',
|
||||
'settings.privacyMetricsHint': 'จำนวนการใช้งาน, การใช้โทเค็น, อัตราข้อผิดพลาด',
|
||||
'settings.privacyContent': 'เนื้อหาการสนทนา',
|
||||
|
|
|
|||
|
|
@ -165,8 +165,9 @@ export const tr: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -176,8 +176,9 @@ export const uk: Dict = {
|
|||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design can share usage data with our team to help us improve. This includes:',
|
||||
'settings.privacyConsentFooter': 'You can change either of these any time in Settings → Privacy. We never upload the contents of your generated artifact files.',
|
||||
'settings.privacyConsentShare': 'Help improve',
|
||||
'settings.privacyConsentDecline': 'Not now',
|
||||
'settings.privacyConsentShare': 'Share usage data',
|
||||
'settings.privacyConsentDecline': "Don't share",
|
||||
'settings.privacyConsentPolicyLink': 'Read the privacy policy',
|
||||
'settings.privacyMetrics': 'Anonymous metrics',
|
||||
'settings.privacyMetricsHint': 'Run counts, token usage, error rate, duration. No prompts, no project data.',
|
||||
'settings.privacyContent': 'Conversation content',
|
||||
|
|
|
|||
|
|
@ -180,8 +180,9 @@ export const zhCN: Dict = {
|
|||
'settings.privacyConsentKicker': '帮助我们改进 Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design 可以将使用数据共享给我们的团队以协助改进。包括:',
|
||||
'settings.privacyConsentFooter': '你可以随时在 设置 → 隐私 中修改任意一项。我们绝不上传你生成的产物文件内容。',
|
||||
'settings.privacyConsentShare': '帮助改进',
|
||||
'settings.privacyConsentDecline': '暂不',
|
||||
'settings.privacyConsentShare': '分享使用数据',
|
||||
'settings.privacyConsentDecline': '不分享',
|
||||
'settings.privacyConsentPolicyLink': '阅读隐私政策',
|
||||
'settings.privacyMetrics': '匿名指标',
|
||||
'settings.privacyMetricsHint': '运行次数、token 用量、错误率、时长。不包含 prompt,不包含项目数据。',
|
||||
'settings.privacyContent': '对话和工具内容',
|
||||
|
|
|
|||
|
|
@ -173,8 +173,9 @@ export const zhTW: Dict = {
|
|||
'settings.privacyConsentKicker': '協助我們改進 Open Design',
|
||||
'settings.privacyConsentLead': 'Open Design 可以將使用資料分享給我們的團隊以協助改進。包含:',
|
||||
'settings.privacyConsentFooter': '你可以隨時在 設定 → 隱私 中變更任一項。我們絕不上傳你產生的產出檔案內容。',
|
||||
'settings.privacyConsentShare': '協助改進',
|
||||
'settings.privacyConsentDecline': '暫不',
|
||||
'settings.privacyConsentShare': '分享使用資料',
|
||||
'settings.privacyConsentDecline': '不分享',
|
||||
'settings.privacyConsentPolicyLink': '閱讀隱私政策',
|
||||
'settings.privacyMetrics': '匿名指標',
|
||||
'settings.privacyMetricsHint': '執行次數、token 用量、錯誤率、時長。不包含 prompt,不包含專案資料。',
|
||||
'settings.privacyContent': '對話內容',
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ export interface Dict {
|
|||
'settings.privacyConsentFooter': string;
|
||||
'settings.privacyConsentShare': string;
|
||||
'settings.privacyConsentDecline': string;
|
||||
'settings.privacyConsentPolicyLink': string;
|
||||
'settings.privacyMetrics': string;
|
||||
'settings.privacyMetricsHint': string;
|
||||
'settings.privacyContent': string;
|
||||
|
|
|
|||
|
|
@ -19525,7 +19525,10 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.privacy-consent-banner .privacy-consent-actions {
|
||||
/* The banner is pointer-events:none so the user can click through the empty
|
||||
card area to the app behind it. Interactive children opt back in. */
|
||||
.privacy-consent-banner .privacy-consent-actions,
|
||||
.privacy-consent-banner .privacy-consent-policy-link {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
|
|
@ -19557,7 +19560,46 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
}
|
||||
|
||||
.privacy-consent-banner-footer {
|
||||
margin: 2px 0 6px;
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
/* Privacy policy link — between the disclosure copy and the action buttons.
|
||||
Underlined and accent-coloured so it's an obvious, distinct affordance,
|
||||
not just tinted text. */
|
||||
.privacy-consent-policy-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.privacy-consent-policy-link:hover {
|
||||
color: color-mix(in srgb, var(--accent) 80%, var(--text));
|
||||
}
|
||||
|
||||
.privacy-consent-policy-link:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* On narrow viewports the fixed bottom-right card crowds page content —
|
||||
stretch it into a bottom-edge bar with even gutters, and clear the
|
||||
mobile browser chrome / home indicator via the safe-area inset. */
|
||||
@media (max-width: 540px) {
|
||||
.privacy-consent-banner {
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
width: auto;
|
||||
max-width: none;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ describe('App connectors settings flows', () => {
|
|||
const banner = container.querySelector('.privacy-consent-banner');
|
||||
expect(banner?.querySelector('.seg-control')).toBeNull();
|
||||
expect(banner?.querySelector('.seg-btn.active')).toBeNull();
|
||||
expect(screen.getByRole('button', { name: 'Help improve' }).className).toContain(
|
||||
expect(screen.getByRole('button', { name: 'Share usage data' }).className).toContain(
|
||||
'privacy-consent-action',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
61
apps/web/tests/components/PrivacyConsentModal.test.tsx
Normal file
61
apps/web/tests/components/PrivacyConsentModal.test.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PrivacyConsentModal } from '../../src/components/PrivacyConsentModal';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
|
||||
const PRIVACY_POLICY_HREF = 'https://github.com/nexu-io/open-design/blob/main/PRIVACY.md';
|
||||
|
||||
function renderModal(overrides?: { onShare?: () => void; onDecline?: () => void }) {
|
||||
const onShare = overrides?.onShare ?? vi.fn();
|
||||
const onDecline = overrides?.onDecline ?? vi.fn();
|
||||
render(
|
||||
<I18nProvider initial="en">
|
||||
<PrivacyConsentModal onShare={onShare} onDecline={onDecline} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
return { onShare, onDecline };
|
||||
}
|
||||
|
||||
describe('PrivacyConsentModal', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it('labels the affirmative action as a consent choice, not "Help improve"', () => {
|
||||
renderModal();
|
||||
expect(screen.getByRole('button', { name: 'Share usage data' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: "Don't share" })).toBeTruthy();
|
||||
// The old label gave no signal that this was a privacy consent decision.
|
||||
expect(screen.queryByRole('button', { name: 'Help improve' })).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the accept and decline buttons equal-prominence (EDPB/GDPR)', () => {
|
||||
renderModal();
|
||||
const share = screen.getByRole('button', { name: 'Share usage data' });
|
||||
const decline = screen.getByRole('button', { name: "Don't share" });
|
||||
// Identical class lists — neither button is styled as primary/secondary.
|
||||
expect(share.className).toBe(decline.className);
|
||||
expect(share.className).toContain('privacy-consent-action');
|
||||
});
|
||||
|
||||
it('exposes the privacy policy via an obvious external link', () => {
|
||||
renderModal();
|
||||
const link = screen.getByRole('link', { name: /privacy policy/i });
|
||||
expect(link.getAttribute('href')).toBe(PRIVACY_POLICY_HREF);
|
||||
expect(link.getAttribute('target')).toBe('_blank');
|
||||
expect(link.getAttribute('rel') ?? '').toContain('noopener');
|
||||
});
|
||||
|
||||
it('invokes the matching handler when each action is clicked', () => {
|
||||
const { onShare, onDecline } = renderModal();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Share usage data' }));
|
||||
expect(onShare).toHaveBeenCalledTimes(1);
|
||||
expect(onDecline).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: "Don't share" }));
|
||||
expect(onDecline).toHaveBeenCalledTimes(1);
|
||||
expect(onShare).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue