feat: 国际化(i18n) en/ja/ko + App 内语言切换
主体:多语言支持(简体中文源 + 英/日/韩)
- 基础设施:Localizable.xcstrings(String Catalog,sourceLanguage=zh-Hans)
+ pbxproj developmentRegion/knownRegions 注册 en/ja/ko
- 全部硬编码 Locale("zh_CN") → Locale.current;中文 dateFormat → Date.FormatStyle(跟随系统)
- UI 中文字面量统一为 String(appLoc:)(显式绑定所选语言 bundle+locale,即时切换)
Text 字面量走环境 \.locale + Bundle 重定向
- 549 个 catalog key 全部 en/ja/ko 翻译完成(0 未翻译)
- App 内语言切换:我的 → 语言(LanguageManager + 即时生效,无需重启)
- 双用预设(症状/监测指标/慢病)本地化:static→computed 避免缓存
注:本提交为 WIP,一并打包了并行进行的功能模块
(HealthExport 健康导出、Security/Face ID 锁、DiaryAssist 日记 AI 辅助)
及 App 图标、CLAUDE.md、docs/scripts。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
130
docs/superpowers/specs/2026-05-30-faceid-app-lock-design.md
Normal file
130
docs/superpowers/specs/2026-05-30-faceid-app-lock-design.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Face ID 启动锁 — 设计文档
|
||||
|
||||
**日期**:2026-05-30(W2)
|
||||
**作者**:link2026 + Claude
|
||||
**关联卖点**:#4 隐私三件套(系统级加密 + Face ID + 永久删除)
|
||||
**优先级**:P1(CLAUDE.md §6 / §8 / §11,原排期 W5 末,提前实现)
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话定位
|
||||
|
||||
可选的 Face ID/Touch ID 启动锁(默认关)。开启后,冷启动与「后台超过 1 分钟再回前台」都需要系统认证才能进入 App;失败可用设备密码兜底。完全基于系统 `LocalAuthentication`,不自造任何密码学(对齐红线 §10.2)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计决策(已与用户确认)
|
||||
|
||||
| 决策点 | 选择 |
|
||||
|---|---|
|
||||
| 锁屏时机 | 冷启动 + 后台超过宽限才重锁 |
|
||||
| 后台宽限 | 60 秒 |
|
||||
| 认证策略 | `.deviceOwnerAuthentication`(Face ID/Touch ID 优先,自动跳设备密码兜底,避免锁死) |
|
||||
| 默认状态 | 关(§6) |
|
||||
| 开关位置 | 「我的」Tab 现有的 Face ID 卡,改为可交互 Toggle |
|
||||
| 任务切换器隐私遮罩 | 加,**仅锁开启时生效**(进 `.inactive`/`.background` 盖品牌遮罩,防多任务快照泄露;默认关用户无感) |
|
||||
|
||||
**关于 §6「截屏黑屏防护…不做」**:那条针对的是**截图防护**(iOS 无官方 API);本设计的任务切换器遮罩是 `.inactive` 盖视图,是官方支持的标准做法,性质不同。
|
||||
|
||||
---
|
||||
|
||||
## 3. 架构
|
||||
|
||||
```
|
||||
KangkangApp
|
||||
└─ WindowGroup { AppLockContainer { RootView() } } ← 仅包一层,RootView 零改动(§10.7)
|
||||
│
|
||||
┌─────────────┴──────────────────────────────┐
|
||||
│ AppLockContainer<Content> │
|
||||
│ @Environment(\.scenePhase) │
|
||||
│ 渲染 content │
|
||||
│ .overlay { if isLocked → LockScreen}│
|
||||
│ .overlay { else if showsCover → PrivacyCover}│
|
||||
│ onAppear → handleAppear(); │
|
||||
│ onChange(scenePhase) → handleScenePhase() │
|
||||
└─────────────────────────────────────────────┘
|
||||
│ 读写
|
||||
┌─────────────┴──────────────────────────────┐
|
||||
│ AppLock.shared (@MainActor @Observable) │ ← Security/AppLock.swift
|
||||
│ enabled ←→ UserDefaults("faceIDLockEnabled")│
|
||||
│ isLocked / showsPrivacyCover │
|
||||
│ biometryAvailable / biometryLabel │
|
||||
│ gracePeriod = 60s,lastBackgroundedAt │
|
||||
│ authenticate() / enableWithAuth() / disable()│
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
单例写法与项目既有 `ModelDownloadService.shared` 一致(`@MainActor @Observable final class` + `static let shared`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 触发逻辑(状态机)
|
||||
|
||||
| scenePhase / 事件 | 行为 |
|
||||
|---|---|
|
||||
| 容器 `onAppear`(冷启动) | `enabled` 为真且尚未冷启动锁过 → `isLocked = true` + 触发认证 |
|
||||
| `.background` | `lastBackgroundedAt = now`;`showsPrivacyCover = enabled` |
|
||||
| `.inactive`(任务切换器) | `showsPrivacyCover = enabled && !isLocked` |
|
||||
| `.active` | 隐藏遮罩;若 `enabled && !isLocked && 离开 > 60s` → `isLocked = true`;若 `isLocked` → 触发认证;清空 `lastBackgroundedAt` |
|
||||
| 认证成功 | `isLocked = false` |
|
||||
| 认证失败/取消 | 保持锁定,锁屏提供「解锁」按钮重试(`isAuthenticating` 防重入,不重复弹窗) |
|
||||
|
||||
冷启动时 scenePhase 初值为 `.active` 不触发 `onChange`,由 `handleAppear()` 负责冷启动锁;两路触发由 `isAuthenticating` 守卫去重。
|
||||
|
||||
---
|
||||
|
||||
## 5. 能力探测与兜底
|
||||
|
||||
- `refreshAvailability()`:`LAContext.canEvaluatePolicy(.deviceOwnerAuthentication)` → `biometryAvailable`;读 `biometryType` 决定文案(Face ID / Touch ID / 密码)。
|
||||
- 设备未设密码/无生物识别 → `biometryAvailable = false`,「我的」开关置灰,副标题「本设备未设置 Face ID 或密码」。
|
||||
- 认证全程系统弹窗;失败/取消不抛错给 UI,只是停留锁屏。
|
||||
|
||||
---
|
||||
|
||||
## 6. 文件清单
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `康康/Security/AppLock.swift` | **新增**:单例 + LAContext 封装 + 触发逻辑 |
|
||||
| `康康/Security/AppLockContainer.swift` | **新增**:包裹层 + scenePhase 驱动 + 两个 overlay |
|
||||
| `康康/Security/LockScreenView.swift` | **新增**:`LockScreenView` + `PrivacyCoverView` |
|
||||
| `康康/App/KangkangApp.swift` | `RootView()` → `AppLockContainer { RootView() }` |
|
||||
| `康康/Features/Me/MeView.swift` | 静态 Face ID 卡 → 可交互 Toggle 卡 |
|
||||
| `康康.xcodeproj/project.pbxproj` | 加 `INFOPLIST_KEY_NSFaceIDUsageDescription`(Debug + Release) |
|
||||
|
||||
工程用文件系统同步组,新增 `Security/` 下的源文件自动纳入编译,无需手改 pbxproj 注册。
|
||||
|
||||
---
|
||||
|
||||
## 7. UI
|
||||
|
||||
锁屏(`LockScreenView`,全遮罩,走 Tj tokens):
|
||||
|
||||
```
|
||||
🔒 (lock glyph)
|
||||
康康 已锁定
|
||||
你的健康档案已加密保护
|
||||
[ Face ID 解锁 ] ← onAppear 自动触发一次认证;按钮文案随设备能力变
|
||||
```
|
||||
|
||||
隐私遮罩(`PrivacyCoverView`):品牌色底 + app 名,无交互,仅用于遮挡多任务快照。
|
||||
|
||||
「我的」Face ID 卡:Toggle 开启时先认证一次(成功才置 `enabled`),关闭直接关。副标题动态:「已开启 · Face ID」/「关闭」/「本设备未设置 Face ID 或密码」。
|
||||
|
||||
---
|
||||
|
||||
## 8. 红线对齐(CLAUDE.md §10)
|
||||
|
||||
- 不自造密码学,只用系统 `LocalAuthentication` ✅
|
||||
- 默认关,可选开关 ✅
|
||||
- 不引云 ✅
|
||||
- 不重构 Tab/RecordSheet 骨架,只加一层包裹 ✅
|
||||
- 清单内功能(§6/§8/§11 明列 Face ID 启动锁)✅
|
||||
|
||||
---
|
||||
|
||||
## 9. 测试与验收
|
||||
|
||||
- 单元测试价值低(核心是系统弹窗 + scenePhase),不强求;`AppLock` 的宽限判定逻辑可抽纯函数测(可选)。
|
||||
- **真机验收**:① 开关开启走 Face ID;② 杀进程冷启动需认证;③ 后台 <60s 回来不锁、>60s 回来锁;④ 多任务切换器快照被遮罩;⑤ 关 Face ID 录入(模拟失败)能跳设备密码;⑥ 默认关时全程无感。
|
||||
- 模拟器:Features → Face ID → Enrolled / Matching Face 可模拟。
|
||||
Reference in New Issue
Block a user