[Bug] Android 移动端 SSO 灰度账号的 OTA 能力依赖设备历史登录状态
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 问题描述 / What happened
PR #3359 将移动端自建 OTA 检查绑定到设备级隐私同意状态 `consent`。
企业 SSO 登录路径明确豁免隐私协议同意,因此首次安装后直接使用 SSO 登录时,设备上的 `consent` 仍为 `false`,OTA 检查会被跳过。
但如果同一设备此前已经有非 SSO 账号登录并同意过隐私协议,随后通过“添加账号”切换到 SSO,设备级 `consent=true` 会被保留,SSO 账号可以进入 OTA 流程。相反,如果先完整退出登录,退出流程会清除 `consent`;之后再登录 SSO,OTA 又会被拦截。
因此,同一个 SSO 灰度账号会因为设备历史登录路径不同,表现为“有时能收到灰度热更新,有时完全收不到”。
期望行为:
- SSO 灰度账号的 OTA 能力不应依赖设备此前是否登录过其他账号或是否完整退出过。
- 首次安装直接 SSO、非 SSO 账号切换到 SSO、退出后重新登录 SSO,这几种路径应有一致且符合隐私合规要求的更新行为。
- 不能简单绕过隐私同意闸门,应明确 SSO 登录与 OTA 更新检查之间的授权语义,或提供不依赖该设备标识的合规更新路径。
## 环境 / Environment
- 平台:Android 真机
- 分发方式:移动端自建 Release APK
- 配套桌面版本:`0.1.72`,commit `2a039176935c`
- 设备:Samsung Android 真机
- iOS:未测试
- 桌面端:未复现
## 复现步骤 / Steps to reproduce
共同前置条件:
1. 使用 Global 移动端自建 Release 包。
2. 准备一个不在目标灰度名单中的非 SSO 账号,以及一个在灰度名单中的企业 SSO 账号。
3. 确保设备没有已下载的 pending OTA,两个分支分别重新安装,避免相互污染。
| 路径 | 操作 | 实际结果 |
| --- | --- | --- |
| A:账号切换 | 非 SSO 账号登录并同意隐私协议;通过“添加账号”登录 SSO;不退出登录,使用账号切换器切到 SSO;检查 OTA | SSO 保留设备级 `consent=true`,可以进入 OTA 流程 |
| B:完整退出 | 非 SSO 账号登录并同意隐私协议;完整退出登录;重新使用 SSO 登录;检查 OTA | 退出时 `consent` 被清除为 `false`,SSO 登录又不会重新写入 consent,无法进入 OTA 流程 |
两条路径的账号资格、设备和安装包保持一致,关键差异是是否经过完整退出登录。
## 日志与截图 / Logs & screenshots
实机验证观察到:
- 路径 A 中,SSO 切换后可以进入 OTA 检查并获取灰度更新。
- 路径 B 中,SSO 重新登录后没有进入新的 OTA manifest/download 流程。
- 两条路径之间的关键差异是完整退出是否调用了 `clearAnalyticsConsent()`。
当前不附原始日志或截图。已有 ADB 日志包含设备、账号和更新资源信息,后续如需附加,应先完全脱敏。
## 源码定位
当前 `origin/main` 中:
- `apps/mobile/src/update/updateConsentGate.ts`:`hasPrivacyConsent()` 直接读取设备级 `analyticsConsentStore.consent`。
- `apps/mobile/src/update/useStartupOtaGate.ts`:`consent === false` 时直接跳过 OTA 检查,不执行 `checkForUpdateAsync()` 或 `fetchUpdateAsync()`。
- `apps/mobile/src/analytics/analyticsConsentStore.ts`:`acceptPrivacyConsent()` 将设备级 consent 写为 `true`;`clearAnalyticsConsent()` 将 consent 清为 `false`,并写入墓碑记录。
- `apps/mobile/app/(auth)/login.tsx`:手机号、邮箱和社交登录走协议同意链路;企业 SSO 入口明确豁免协议同意,因此 SSO 登录本身不会调用 `acceptPrivacyConsent()`。
- `apps/mobile/src/auth/AuthContext.tsx`:账号切换路径保留同意状态;完整退出的 `clearLocalSession()` 会调用 `clearAnalyticsConsent()`。
因此,当前实现把“隐私同意”作为设备级状态,却让 SSO 登录绕过写入;这使 SSO OTA 结果受设备历史账号行为影响。
## 相关记录
- [PR #3359](https://github.com/makecindy/cindy/pull/3359):引入 Mobile OTA 隐私同意闸门。
- [Issue #3362](https://github.com/makecindy/cindy/issues/3362):PR #3359 的维护者确认记录,已关闭,未覆盖本次实机回归。
- [Issue #408](https://github.com/makecindy/cindy/issues/408):讨论过 SSO 与设备级隐私同意的后续语义问题。
- [PR #3019](https://github.com/makecindy/cindy/pull/3019):引入 Mobile 设备级 Beta 默认策略;其设备级通道状态也应与本问题一并核对,但本次现象的确定性差异来自 consent 状态。
## 建议方向
在不重新引入“未同意时发送带 `eas-client-id` 请求”的隐私风险前提下,重新定义 SSO 用户的 OTA 更新资格,使以下路径结果一致:
1. 首次安装直接 SSO 登录。
2. 非 SSO 登录后通过账号切换进入 SSO。
3. 完整退出后重新登录 SSO。
同时补充 Android 回归测试,分别验证:
- SSO-only 冷启动不会因为设备无历史 consent 而永久失去灰度更新入口。
- 账号切换与完整退出后的 SSO 行为符合最终产品和隐私决策。
- 测试不会被已有 pending OTA 或设备级 Beta/canary 状态污染。
Contributor guide
Research direction
Read apps/mobile/src/update/updateConsentGate.ts, apps/mobile/src/update/useStartupOtaGate.ts, analyticsConsentStore.ts, login.tsx, and AuthContext.tsx to trace consent through SSO, account switching, and logout. Review PR #3359 and Issues #3362/#408, then add Android regression coverage for the three login paths; done means their OTA behavior is consistent without bypassing the privacy gate or allowing device state to contaminate tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, react-native, typescript
- Domain
- authentication, mobile, release
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100