Files
social-app/docs/superpowers/specs/2026-03-19-calendar-reminder-unified-interaction-design.md
T

216 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 日历提醒统一交互设计(iOS/Android
## 1. 背景与问题
当前日历提醒模块存在以下问题:
1. iOS 通知动作("稍后提醒"/"取消")在横幅场景下可见性不稳定,用户误判为无按钮。
2. Android 与 iOS 的通知动作回调链路不一致,导致按钮点击在部分状态下无效。
3. 前台提醒体验依赖系统默认样式,Android 观感较弱,不符合产品视觉语言。
4. 提醒动作与弹窗交互代码存在历史分叉,维护成本高,且存在潜在无用旧代码残留。
## 2. 目标与非目标
### 2.1 目标
- 支持 App 关闭状态下的到点提醒(系统通知主触达)。
- 统一提醒动作语义:
- `稍后提醒` = 延后 10 分钟。
- `取消` = 归档日历事件。
- 前台状态提供一套跨平台复用的应用内提醒面板,提升视觉质量。
- 无论动作来自系统通知还是应用内面板,都进入同一业务执行链路。
- 在新链路稳定后,清除无用旧代码与重复入口。
### 2.2 非目标
- 不改变提醒策略(仍按当前 reminderMinutes + 重复提醒策略)。
- 不改动日历事件核心数据结构与后端协议。
- 不在本次引入新的提醒类型(例如自定义延后时长、多级动作)。
## 3. 总体方案
采用“系统通知主触达 + 前台应用内面板增强”的混合方案:
1. **系统通知层(平台差异化)**
- Android/iOS 继续使用 `flutter_local_notifications`
- 平台分别补齐通知动作接收能力,确保前台/后台/终止态都可触发动作。
2. **动作执行层(跨平台统一)**
-`ReminderActionExecutor` 作为唯一动作入口。
- 内部动作 ID 固定为:`ReminderAction.snooze10m``ReminderAction.archive`
- UI 文案中的“取消”仅为展示文案,内部统一映射到 `archive`
3. **前台呈现层(跨平台复用)**
- 新增应用内 `ReminderActionSheet`(共享组件,遵循设计 token)。
- 仅在应用前台触发,用于替代系统默认弹窗体验。
4. **展示策略(避免双提醒)**
- 前台(App active):默认只展示 `ReminderActionSheet`,不展示系统通知横幅。
- 后台/终止态:只展示系统通知。
## 4. 关键设计决策
### 4.1 是否需要 iOS/Android 各写一套弹窗组件
不需要。应用内提醒组件采用一套 Flutter 共享实现。
需要分平台处理的是系统通知配置与回调桥接,不是应用内 UI 组件本身。
### 4.2 到点提醒是否继续使用“弹窗”
主流做法是系统通知,不是纯应用内弹窗。原因:
- App 关闭态仅系统通知可达。
- 锁屏、通知中心具备天然可达性与系统一致性。
- 可直接承载动作按钮(稍后提醒、取消并归档)。
前台场景再补应用内面板,兼顾体验与一致行为。
### 4.3 iOS 动作按钮显示问题
iOS 横幅通常不保证直接展示全部动作按钮,需展开通知查看动作。该行为属于系统 UI 规则。
此外 iOS 通知 category 存在缓存特性,category 变更后可能需要重装或升级 category id 才能稳定生效。
## 5. 模块与职责划分
### 5.1 保留并增强
- `apps/lib/core/notifications/local_notification_service.dart`
- 仅负责通知调度与平台动作回调桥接。
- 不负责前台 UI 展示。
- 补齐后台动作回调接入。
- `apps/lib/features/calendar/reminders/reminder_action_executor.dart`
- 作为唯一动作执行器。
- 保持归档 outbox 重试机制。
### 5.2 新增
- `apps/lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart`
- 感知 app 前后台状态。
- 前台触发应用内提醒面板。
- 作为前台展示唯一入口,禁止其他模块直接弹出提醒面板。
- `ReminderActionSheet`(共享组件)
- 展示事件摘要 + 两个动作按钮。
- 保证与系统通知动作语义一致。
### 5.3 平台接入补齐
- Android:
-`apps/android/app/src/main/AndroidManifest.xml` 增加 `ActionBroadcastReceiver`
- 配置并接入 `onDidReceiveBackgroundNotificationResponse`
- Android 13+ 先做 `POST_NOTIFICATIONS` 授权检查;未授权时降级应用内提示并记录埋点。
- 后台回调函数必须为 top-level 且加 `@pragma('vm:entry-point')`
- iOS:
-`apps/ios/Runner/AppDelegate.swift` 配置 plugin registrant callback(用于后台 action isolate)。
- 后台回调函数必须为 top-level 且加 `@pragma('vm:entry-point')`
- `UNNotificationCategory` 在应用启动早期完成注册(早于提醒调度)。
- 为 category id 增加版本化策略(`calendar_reminder_v{n}`),避免缓存导致动作更新不生效。
## 6. 动作流转(统一)
### 6.1 稍后提醒
触发源(系统通知按钮或应用内面板按钮)
-> 统一映射为 `ReminderAction.snooze10m`
-> `ReminderActionExecutor._snoozeEvent`
-> 重新计算下一次时间并 `scheduleReminderAt`
### 6.2 取消(归档)
触发源(系统通知按钮或应用内面板按钮)
-> 统一映射为 `ReminderAction.archive`
-> 取消本地提醒
-> 写 outbox 并调用归档接口
-> 成功标记 done,失败进入 retry/backoff
### 6.3 动作回传契约(前台/后台/终止态统一)
- 每次动作生成幂等键:`actionExecutionId = notificationId + actionId + fireTimeBucket`
- 执行前先查重(本地持久化幂等表);命中时直接 ACK,不重复执行业务副作用。
- 终止态动作进入 cold-start queue 回放,按接收时间顺序处理。
- 单条动作失败不阻塞后续动作;失败进入 retry/backoff 并可观测。
## 7. 旧代码收集与清理计划
### 7.1 旧代码清单建立
改造前先建立“提醒模块迁移清单”,按三类标记:
- `保留`:仍由新架构使用。
- `替换`:保留接口,重写实现。
- `删除`:无引用、重复职责、历史临时逻辑。
迁移清单字段必须包含:`文件路径``符号名``处理决策(保留/替换/删除)``责任人`
### 7.2 清理时机
- 新链路在 Android+iOS 均验证通过后,立即执行删除。
- 不做“先保留一版再说”的长期并存。
### 7.3 清理范围
- 无效弹窗触发入口。
- 不再使用的提醒动作映射分支。
- 重复回调注册与过时常量(旧 action id / 旧 category id)。
- 不再有保护价值的旧测试与旧 fixture。
### 7.4 清理验收
- 以“旧标识 0 引用”为验收标准,至少覆盖:旧 action id、旧 category id、旧入口函数名。
- 输出固定 grep 关键字清单并逐条验收。
- 提醒链路测试通过。
- 删除项对应测试通过,且无悬挂 fixture/snapshot 引用。
- 手工回归覆盖前台/后台/终止态三种状态。
## 8. 测试与验证
### 8.1 自动化
- 新增/更新 reminders 相关单测:
- 动作映射正确性(notification/app sheet -> executor)。
- `archive` 的 outbox 行为与重试退避逻辑。
- `snooze10m` 在边界时间的调度行为。
- 同一 `actionExecutionId` 重复投递仅执行一次(幂等)。
- 终止态 cold-start queue 回放不丢失且顺序一致。
- 前台面板与系统通知并发触发时仅产生一次业务副作用。
### 8.2 手工验证矩阵
- Android:
- 前台:面板按钮可用。
- 后台:通知动作可用。
- 杀进程:通知动作可用。
- iOS:
- 前台:面板按钮可用。
- 后台/锁屏:通知展开后动作可用。
- 安装升级后 category 动作可用。
## 9. 风险与缓解
- **风险**iOS category 缓存导致动作更新不生效。
- **缓解**category id 版本化 + 明确重装验证步骤。
- **风险**:后台动作 isolate 未正确注册导致点击丢失。
- **缓解**AppDelegate/Manifest 严格按插件要求配置,并做终止态回归。
- **风险**:前台面板与系统通知并发触发造成重复操作。
- **缓解**PresentationCoordinator 增加去重窗口与事件级幂等保护。
- **风险**:通知权限未授权导致后台提醒不可达。
- **缓解**:启动期权限检查 + 降级提示 + 埋点追踪。
## 10. 完成定义(DoD
- App 关闭状态下,系统通知可触达并可执行两个动作。
- `取消` 在业务上严格等价于归档。
- 前台统一提醒面板上线,Android 样式符合项目视觉语言。
- 动作执行链路唯一,平台仅保留桥接差异。
- 历史无用代码完成清理,且通过验证。
- 手工矩阵 6/6 场景通过(Android 前台/后台/杀进程 + iOS 前台/后台锁屏/升级后)。
- 动作日志可追踪,重复执行率=0(基于 `actionExecutionId` 统计)。