makecindy / makecindy/cindy

[Bug] Android 移动端 SSO 灰度账号的 OTA 能力依赖设备历史登录状态

Open
#3,823 1 comment 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.