Files
eryao/.trellis/tasks/04-17-feat-privacy-do-not-sell/prd.md
T
qzl 913ed26f8d 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
2026-04-17 13:11:09 +08:00

7.5 KiB
Raw Blame History

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 对象

新增字段:

# 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 内容
    • 不添加开关(避免重复功能,保持文档阅读纯粹性)

代码结构:

// 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. 数据模型升级: privacyMap 升级为结构化 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),不影响本次需求