feat(privacy): add personalized ads toggle in settings

- Add PrivacySettings schema with can_sell field (default: false)
- Move privacy toggle to GeneralSettingsScreen with clear labeling
- Update l10n for zh/en/zh_hant with user-friendly copy
- Clean up redundant PrivacyNotificationSettingsScreen
- Update profile-protocol.md to reflect new privacy schema
- Fix basedpyright warnings in user.py
This commit is contained in:
qzl
2026-04-17 13:11:09 +08:00
parent be30eb6eab
commit 913ed26f8d
17 changed files with 417 additions and 163 deletions
@@ -0,0 +1,7 @@
{"order": 1, "check": "Backend PrivacySettings class exists", "file": "backend/src/schemas/shared/user.py", "criteria": "PrivacySettings class with do_not_sell: bool = True, profileVisibility: str, personalization: bool, historyVisibility: str"}
{"order": 2, "check": "Backend ProfileSettingsV1.privacy uses PrivacySettings type", "file": "backend/src/schemas/shared/user.py", "criteria": "privacy: PrivacySettings field (not dict) in ProfileSettingsV1"}
{"order": 3, "check": "Frontend PrivacySettings class exists", "file": "apps/lib/features/settings/data/models/profile_settings.dart", "criteria": "PrivacySettings class with doNotSell, profileVisibility, personalization, historyVisibility, copyWith method"}
{"order": 4, "check": "Frontend ProfileSettingsV1.privacy uses PrivacySettings type", "file": "apps/lib/features/settings/data/models/profile_settings.dart", "criteria": "privacy: PrivacySettings field (not Map) in ProfileSettingsV1"}
{"order": 5, "check": "PrivacyNotificationSettingsScreen has DoNotSell switch", "file": "apps/lib/features/settings/presentation/screens/privacy_notification_settings_screen.dart", "criteria": "SettingsSwitchTile with Do Not Sell label at top of Privacy section, Profile Visibility placeholder still exists"}
{"order": 6, "check": "LegalCenterScreen has DoNotSell shortcut", "file": "apps/lib/features/settings/presentation/screens/legal_center_screen.dart", "criteria": "SettingsMenuTile with Do Not Sell label, shows current status (enabled/disabled), navigates to PrivacyNotificationSettingsScreen"}
{"order": 7, "check": "L10n keys added (all locales)", "file": "apps/lib/l10n/", "criteria": "settingsDoNotSellTitle, settingsDoNotSellDescription, settingsDoNotSellEnabled, settingsDoNotSellDisabled keys exist in app_en.arb, app_zh.arb, app_zh_hant.arb"}
@@ -0,0 +1,7 @@
{"order": 1, "step": "Backend: Add PrivacySettings class", "file": "backend/src/schemas/shared/user.py", "action": "Create PrivacySettings class with do_not_sell (default=True) + future extension fields", "status": "completed"}
{"order": 2, "step": "Backend: Update ProfileSettingsV1.privacy type", "file": "backend/src/schemas/shared/user.py", "action": "Change privacy from dict[str, object] to PrivacySettings, add backward compatibility logic", "status": "completed"}
{"order": 3, "step": "Frontend: Add PrivacySettings class", "file": "apps/lib/features/settings/data/models/profile_settings.dart", "action": "Create PrivacySettings class with doNotSell + future fields, add copyWith", "status": "completed"}
{"order": 4, "step": "Frontend: Update ProfileSettingsV1.privacy type", "file": "apps/lib/features/settings/data/models/profile_settings.dart", "action": "Change privacy from Map to PrivacySettings type", "status": "completed"}
{"order": 5, "step": "Frontend: Add DoNotSell switch to PrivacyNotificationSettingsScreen", "file": "apps/lib/features/settings/presentation/screens/privacy_notification_settings_screen.dart", "action": "Add SettingsSwitchTile for Do Not Sell before Profile Visibility placeholder", "status": "completed"}
{"order": 6, "step": "Frontend: Add DoNotSell shortcut to LegalCenterScreen", "file": "apps/lib/features/settings/presentation/screens/legal_center_screen.dart", "action": "Add SettingsMenuTile showing current status, navigate to PrivacyNotificationSettingsScreen", "status": "completed"}
{"order": 7, "step": "Frontend: Add l10n keys", "file": "apps/lib/l10n/", "action": "Add Do Not Sell related translations (zh/en/zh_Hant): title, description, enabled, disabled", "status": "completed"}
@@ -0,0 +1,190 @@
# PRD: Do Not Sell My Personal Information 开关
## 1. 功能需求概述
实现 CCPA/CPRA 合规的 "Do Not Sell My Personal Information" 功能:
- 隐私政策页添加 Do Not Sell 开关(胶囊状态栏切换)
- 设置 → 隐私页添加相同开关
- 后端存储用户隐私偏好
- 开关默认开启(用户选择不被销售)
## 2. 背景与合规要求
### CCPA/CPRA 要求
- 如果企业"销售"或"共享"个人信息用于广告,必须提供退出选项
- 即使声明不卖,仍需提供退出机制作为合规保险
- App Store 审核会检查此功能的存在
### 业务现状
- 隐私政策已声明 `WE DO NOT SELL YOUR PERSONAL INFORMATION`
- 但缺少用户端开关实现
## 3. 技术方案
### 3.1 后端实现
**存储位置**: `profiles.settings.privacy` (JSONB)
**设计决策**: 将 `privacy` 字段升级为结构化的 `PrivacySettings` 对象
**新增字段**:
```python
# backend/src/schemas/shared/user.py
class PrivacySettings(BaseModel):
"""隐私相关设置统一管理"""
do_not_sell: bool = True # 本次需求:Do Not Sell(默认 True
profile_visibility: str = "public" # 未来扩展:个人资料可见性
# ProfileSettingsV1 中替换
class ProfileSettingsV1(BaseModel):
privacy: PrivacySettings = Field(default_factory=PrivacySettings) # 替换 dict
```
**API 变更**: 复用现有 `PATCH /users/me/settings` 接口,无需新增 endpoint
**数据库迁移**: 无需 Schema 变更(privacy 是 JSONB,可自由扩展)
**向后兼容**:
- 现有 `privacy: dict` 数据会自动转换为 `PrivacySettings`
- 新字段使用默认值,不影响现有用户
### 3.2 Flutter 前端实现
**数据层**:
- 新增 `PrivacySettings` class(对应后端)
- `ProfileSettingsV1.privacy` 类型从 `Map<String, Object?>` 改为 `PrivacySettings`
- 默认值:`doNotSell = true`(默认开启)
**UI 实现**:
1. **主开关:隐私通知设置页** (`PrivacyNotificationSettingsScreen`)
- 在"隐私" section 下(最前面)添加 "Do Not Sell" 开关
- 复用 `SettingsSwitchTile` 组件(已有)
- 文案: "Limit Use of My Personal Information"
- 默认值: True(开启)
- 开关状态实时保存
- "Profile Visibility" 保持现状(占位符,显示 Coming Soon
2. **快捷入口:法律中心列表页** (`LegalCenterScreen`)
- 在"隐私" section 下(法律文档列表之后)添加独立按钮
- 不是开关,而是快捷入口(SettingsMenuTile
- 显示当前状态:已开启 / 已关闭
- 点击跳转到 `PrivacyNotificationSettingsScreen`
- 满足 CCPA/CPRA 隐私政策中提供"Do Not Sell"入口的要求
3. **隐私政策详情页** (`LegalDocumentScreen`)
- 保持现状:只显示静态 Markdown 内容
- **不添加开关**(避免重复功能,保持文档阅读纯粹性)
**代码结构**:
```dart
// apps/lib/features/settings/data/models/profile_settings.dart
class PrivacySettings {
const PrivacySettings({
this.doNotSell = true,
this.profileVisibility = 'public',
this.personalization = false,
this.historyVisibility = 'private',
});
final bool doNotSell;
final String profileVisibility;
final bool personalization;
final String historyVisibility;
PrivacySettings copyWith({...});
}
class ProfileSettingsV1 {
final PrivacySettings privacy; // 替换 Map<String, Object?>
// ...
}
```
### 3.3 国际化
需要新增以下 key(示例):
- `settingsDoNotSellTitle`: "Do Not Sell My Personal Information"
- `settingsDoNotSellDescription`: "Limit the use of your personal information"
- `settingsDoNotSellHint`: "When enabled, we will not sell or share your data"
## 4. 实现步骤
### Phase 1: 后端数据模型
- [ ] **Backend**: `schemas/shared/user.py` - 新增 `PrivacySettings` class
- 添加 `do_not_sell: bool = True`(本次需求)
- 添加未来扩展字段:`profile_visibility`, `personalization`, `history_visibility`
- [ ] **Backend**: `schemas/shared/user.py` - 更新 `ProfileSettingsV1`
-`privacy: dict[str, object]` 改为 `privacy: PrivacySettings`
- 添加 `parse_profile_settings` 兼容性逻辑(dict → PrivacySettings
### Phase 2: 前端数据模型
- [ ] **Flutter**: `settings/data/models/profile_settings.dart` - 新增 `PrivacySettings` class
- 包含 `doNotSell`, `profileVisibility`, `personalization`, `historyVisibility`
- 实现 `copyWith` 方法
- [ ] **Flutter**: `settings/data/models/profile_settings.dart` - 更新 `ProfileSettingsV1`
-`privacy` 类型从 `Map<String, Object?>` 改为 `PrivacySettings`
### Phase 3: UI 实现
- [ ] **Flutter**: 扩展 `PrivacyNotificationSettingsScreen`
- 在"隐私" section 最前面添加 Do Not Sell 开关(SettingsSwitchTile
- 保持 Profile Visibility 占位符不变
- [ ] **Flutter**: 扩展 `LegalCenterScreen`
- 添加快捷入口按钮(SettingsMenuTile
- 显示当前状态:已开启 / 已关闭
- 实现导航到 `PrivacyNotificationSettingsScreen`
### Phase 4: 业务逻辑
- [ ] 开关状态持久化(通过 `PATCH /users/me/settings`
- [ ] 法律中心快捷按钮显示当前状态(从 settings.privacy.doNotSell 读取)
### Phase 5: 国际化
- [ ] 添加中/英/繁三语 ARB keys
- `settingsDoNotSellTitle`: Do Not Sell My Personal Information
- `settingsDoNotSellDescription`: Limit use of your personal information
- `settingsDoNotSellEnabled`: Enabled
- `settingsDoNotSellDisabled`: Disabled
- [ ] 执行 `flutter gen-l10n`
## 5. 相关代码文件
### Backend
| 文件 | 说明 | 变更类型 |
|------|------|--------|
| `backend/src/schemas/shared/user.py` | 新增 PrivacySettings classProfileSettingsV1.privacy 改为 PrivacySettings 类型 | 修改 |
| `backend/src/v1/users/router.py` | 用户设置 API(复用 PATCH /me/settings | 无变更 |
| `backend/src/v1/users/service.py` | 设置更新逻辑 | 无变更 |
### Frontend
| 文件 | 说明 | 优先级 |
|------|------|--------|
| `apps/lib/features/settings/data/models/profile_settings.dart` | 新增 PrivacySettings classProfileSettingsV1.privacy 类型修改 | P0 |
| `apps/lib/features/settings/presentation/screens/privacy_notification_settings_screen.dart` | 添加 Do Not Sell 开关(主开关),保持 Profile Visibility 占位符 | P0 |
| `apps/lib/features/settings/presentation/screens/legal_center_screen.dart` | 添加快捷入口按钮 | P1 |
| `apps/lib/features/settings/presentation/widgets/settings_section_widgets.dart` | 复用 SettingsSwitchTile(已有) | - |
## 6. 测试计划
- [ ] 开关默认状态为开启
- [ ] 开关切换后值正确保存到后端
- [ ] App 重启后设置正确恢复
- [ ] 中/英/繁语言正确显示
## 7. 注意事项
1. **数据模型升级**: `privacy``Map` 升级为结构化 `PrivacySettings` 对象
2. **向后兼容**: 后端需要兼容现有 `privacy: dict` 数据的读取
3. **合规文案**: 使用 "Limit Use" 而非 "We Don't Sell",避免直接承诺
4. **默认值**: 默认开启(opt-out),符合隐私保护趋势
5. **开关位置**: 主开关在隐私设置页,法律中心只提供快捷入口(避免重复)
6. **开关形式**: 使用 `SettingsSwitchTile` 组件(已有)
7. **法律中心实现**: 添加快捷按钮(SettingsMenuTile),显示当前状态,点击跳转
8. **后端兼容性**: 由于使用 JSONB 扩展,无需数据库迁移
9. **状态同步**: 确保法律中心快捷按钮显示的状态与实际设置一致
10. **Profile Visibility**: 保持现状(占位符 + Coming Soon),不影响本次需求
@@ -0,0 +1,31 @@
{
"name": "feat-privacy-do-not-sell",
"title": "Do Not Sell My Personal Information 开关",
"dev_type": "fullstack",
"created_at": "2026-04-17",
"status": "in_progress",
"worktree": "feat-privacy-do-not-sell",
"description": "实现 CCPA/CPRA 合规的 Do Not Sell My Personal Information 开关功能:将 privacy 字段升级为结构化 PrivacySettings 对象,主开关在隐私通知设置页,法律中心列表页提供快捷入口,后端存储用户偏好,默认开启(不卖)。",
"prd": "prd.md",
"related_docs": [
"docs/discussions/legal-compliance-us.md",
"https://uidqlzahr8w.feishu.cn/wiki/As6AwJRImilu4Lk0lNtcrbmXnOc"
],
"checklist": {
"backend": [
"Create PrivacySettings class with do_not_sell + future fields",
"Update ProfileSettingsV1.privacy to use PrivacySettings type",
"Add backward compatibility logic for existing dict data",
"Verify PATCH /users/me/settings works with new PrivacySettings"
],
"frontend": [
"Create PrivacySettings class matching backend schema",
"Update ProfileSettingsV1.privacy to use PrivacySettings type",
"Add SettingsSwitchTile to PrivacyNotificationSettingsScreen (main switch)",
"Keep Profile Visibility placeholder unchanged",
"Add SettingsMenuTile shortcut to LegalCenterScreen (CCPA compliance)",
"Implement navigation from LegalCenter to PrivacyNotificationSettings",
"Add l10n keys (zh/en/zh_Hant): title, description, enabled, disabled"
]
}
}