392 lines
16 KiB
Markdown
392 lines
16 KiB
Markdown
# Calendar Reminder Unified Interaction Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 交付“系统通知主触达 + 前台统一提醒面板”的提醒链路,保证 iOS/Android 在前台、后台、终止态都可执行“稍后提醒”和“取消并归档(内部 archive)”。
|
||
|
||
**Architecture:** `LocalNotificationService` 仅做调度与平台桥接;`ReminderActionExecutor` 作为唯一动作执行器;新增 `ReminderPresentationCoordinator` + `ReminderActionSheet` 负责前台展示;新增持久化幂等与 cold-start 回放,避免动作丢失/重复执行。
|
||
|
||
**Tech Stack:** Flutter, flutter_local_notifications, SharedPreferences, Dart isolate callback, AndroidManifest receiver, iOS AppDelegate callback, Flutter tests.
|
||
|
||
---
|
||
|
||
## File Structure (Locked Before Tasks)
|
||
|
||
### Protocol (Must First)
|
||
- Modify: `docs/protocols/calendar/reminder-alert-lifecycle.md`
|
||
|
||
### Reminder Core
|
||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||
- Create: `apps/lib/core/notifications/reminder_notification_callbacks.dart`
|
||
- Modify: `apps/lib/features/calendar/reminders/models/reminder_action.dart`
|
||
- Modify: `apps/lib/features/calendar/reminders/reminder_action_executor.dart`
|
||
- Create: `apps/lib/features/calendar/reminders/reminder_action_dedupe_store.dart`
|
||
- Create: `apps/lib/features/calendar/reminders/reminder_cold_start_queue.dart`
|
||
|
||
### Foreground UI
|
||
- Create: `apps/lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart`
|
||
- Create: `apps/lib/features/calendar/reminders/ui/widgets/reminder_action_sheet.dart`
|
||
|
||
### App & Platform Wiring
|
||
- Modify: `apps/lib/main.dart`
|
||
- Modify: `apps/android/app/src/main/AndroidManifest.xml`
|
||
- Modify: `apps/ios/Runner/AppDelegate.swift`
|
||
|
||
### Tests
|
||
- Modify: `apps/test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||
- Create: `apps/test/features/calendar/reminders/reminder_action_dedupe_store_test.dart`
|
||
- Create: `apps/test/features/calendar/reminders/reminder_cold_start_queue_test.dart`
|
||
- Create: `apps/test/features/calendar/reminders/reminder_notification_bridge_test.dart`
|
||
- Create: `apps/test/features/calendar/reminders/reminder_presentation_coordinator_test.dart`
|
||
- Create: `apps/test/features/calendar/reminders/reminder_action_sheet_test.dart`
|
||
- Create: `apps/test/features/calendar/reminders/reminder_permission_fallback_test.dart`
|
||
- Create: `apps/test/platform/android_manifest_notification_action_test.dart`
|
||
- Create: `apps/test/platform/ios_app_delegate_notification_callback_test.dart`
|
||
|
||
### Cleanup
|
||
- Create: `docs/todo/calendar-reminder-migration-checklist.md`
|
||
|
||
---
|
||
|
||
### Task 0: 协议先行更新(必须)
|
||
|
||
**Files:**
|
||
- Modify: `docs/protocols/calendar/reminder-alert-lifecycle.md`
|
||
|
||
- [ ] **Step 1: 更新动作语义文案**
|
||
```text
|
||
展示文案“取消”映射到内部动作 archive
|
||
```
|
||
- [ ] **Step 2: 增加幂等键协议**
|
||
```text
|
||
actionExecutionId = notificationId + actionId + fireTimeBucket
|
||
```
|
||
- [ ] **Step 3: 增加 cold-start queue 回放协议**
|
||
```text
|
||
顺序回放,单条失败不阻塞后续
|
||
```
|
||
- [ ] **Step 4: 运行协议自检**
|
||
Run: `rg "archive|actionExecutionId|cold-start" docs/protocols/calendar/reminder-alert-lifecycle.md`
|
||
Expected: 命中新增规则
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add docs/protocols/calendar/reminder-alert-lifecycle.md
|
||
git commit -m "docs: align reminder protocol with archive and idempotency"
|
||
```
|
||
|
||
### Task 1: 动作语义收敛(TDD)
|
||
|
||
**Files:**
|
||
- Modify: `apps/test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||
- Modify: `apps/lib/features/calendar/reminders/models/reminder_action.dart`
|
||
- Modify: `apps/lib/features/calendar/reminders/reminder_action_executor.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(archive 入口)**
|
||
```dart
|
||
test('archive action cancels reminder and archives event', () async {});
|
||
```
|
||
- [ ] **Step 2: 运行失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 3: 最小实现(增加 archive 枚举并接入 executor)**
|
||
```dart
|
||
enum ReminderAction { archive('archive'), snooze10m('snooze_10m'), ... }
|
||
```
|
||
- [ ] **Step 4: 运行通过测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add apps/lib/features/calendar/reminders/models/reminder_action.dart apps/lib/features/calendar/reminders/reminder_action_executor.dart apps/test/features/calendar/reminders/reminder_action_executor_test.dart
|
||
git commit -m "refactor: unify reminder action semantics to archive"
|
||
```
|
||
|
||
### Task 2: 持久化幂等(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/features/calendar/reminders/reminder_action_dedupe_store_test.dart`
|
||
- Create: `apps/lib/features/calendar/reminders/reminder_action_dedupe_store.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(重启后仍去重)**
|
||
```dart
|
||
test('same actionExecutionId is rejected after restart', () async {});
|
||
```
|
||
- [ ] **Step 2: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_action_dedupe_store_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 3: 实现最小 store**
|
||
```dart
|
||
Future<bool> markIfNew(String actionExecutionId)
|
||
```
|
||
- [ ] **Step 4: 跑通过测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_action_dedupe_store_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add apps/lib/features/calendar/reminders/reminder_action_dedupe_store.dart apps/test/features/calendar/reminders/reminder_action_dedupe_store_test.dart
|
||
git commit -m "feat: add persistent reminder action dedupe store"
|
||
```
|
||
|
||
### Task 3: 冷启动回放队列(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/features/calendar/reminders/reminder_cold_start_queue_test.dart`
|
||
- Create: `apps/lib/features/calendar/reminders/reminder_cold_start_queue.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(顺序回放)**
|
||
```dart
|
||
test('replays actions in receive order', () async {});
|
||
```
|
||
- [ ] **Step 2: 写失败测试(单条失败不阻塞)**
|
||
```dart
|
||
test('continues replay after one action failure', () async {});
|
||
```
|
||
- [ ] **Step 3: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_cold_start_queue_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 4: 最小实现队列**
|
||
```dart
|
||
Future<void> replaySequentially(Future<void> Function(...) handler)
|
||
```
|
||
- [ ] **Step 5: 跑通过测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_cold_start_queue_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 6: Commit**
|
||
```bash
|
||
git add apps/lib/features/calendar/reminders/reminder_cold_start_queue.dart apps/test/features/calendar/reminders/reminder_cold_start_queue_test.dart
|
||
git commit -m "feat: add reminder cold-start replay queue"
|
||
```
|
||
|
||
### Task 4: 通知桥接映射(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/features/calendar/reminders/reminder_notification_bridge_test.dart`
|
||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(cancel 文案映射 archive)**
|
||
```dart
|
||
test('maps cancel action button to ReminderAction.archive', () async {});
|
||
```
|
||
- [ ] **Step 2: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_notification_bridge_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 3: 接入 dedupe store + bridge**
|
||
```dart
|
||
if (!await dedupeStore.markIfNew(actionExecutionId)) return;
|
||
```
|
||
- [ ] **Step 4: 跑通过测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_notification_bridge_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add apps/lib/core/notifications/local_notification_service.dart apps/test/features/calendar/reminders/reminder_notification_bridge_test.dart
|
||
git commit -m "feat: map reminder notification actions through unified bridge"
|
||
```
|
||
|
||
### Task 5: 前台协调器(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/features/calendar/reminders/reminder_presentation_coordinator_test.dart`
|
||
- Create: `apps/lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(仅前台展示)**
|
||
```dart
|
||
test('shows reminder sheet only in app active state', () async {});
|
||
```
|
||
- [ ] **Step 2: 写失败测试(去重窗口)**
|
||
```dart
|
||
test('suppresses duplicate presentation in dedupe window', () async {});
|
||
```
|
||
- [ ] **Step 3: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_presentation_coordinator_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 4: 最小实现协调器**
|
||
- [ ] **Step 5: 跑通过测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_presentation_coordinator_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 6: Commit**
|
||
```bash
|
||
git add apps/lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart apps/test/features/calendar/reminders/reminder_presentation_coordinator_test.dart
|
||
git commit -m "feat: add reminder foreground presentation coordinator"
|
||
```
|
||
|
||
### Task 6: 前台提醒面板组件(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/features/calendar/reminders/reminder_action_sheet_test.dart`
|
||
- Create: `apps/lib/features/calendar/reminders/ui/widgets/reminder_action_sheet.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(稍后提醒按钮)**
|
||
```dart
|
||
testWidgets('tap snooze triggers snooze callback', (tester) async {});
|
||
```
|
||
- [ ] **Step 2: 写失败测试(归档按钮)**
|
||
```dart
|
||
testWidgets('tap archive triggers archive callback', (tester) async {});
|
||
```
|
||
- [ ] **Step 3: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_action_sheet_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 4: 最小实现 token 驱动 UI**
|
||
- [ ] **Step 5: 跑通过测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_action_sheet_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 6: Commit**
|
||
```bash
|
||
git add apps/lib/features/calendar/reminders/ui/widgets/reminder_action_sheet.dart apps/test/features/calendar/reminders/reminder_action_sheet_test.dart
|
||
git commit -m "feat: add reusable reminder action sheet"
|
||
```
|
||
|
||
### Task 7: App 接线与后台入口(TDD)
|
||
|
||
**Files:**
|
||
- Modify: `apps/lib/main.dart`
|
||
- Create/Modify: `apps/lib/core/notifications/reminder_notification_callbacks.dart`
|
||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||
- Create: `apps/test/features/calendar/reminders/reminder_notification_callbacks_test.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(后台入口是 top-level + pragma)**
|
||
```dart
|
||
test('background notification callback is top-level entry-point', () async {});
|
||
```
|
||
- [ ] **Step 2: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_notification_callbacks_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 3: 接线 initialize 注册前台/后台回调**
|
||
- [ ] **Step 4: 接线 foreground presenter 到 coordinator**
|
||
- [ ] **Step 5: 接线 action handler 到 executor**
|
||
- [ ] **Step 6: reminders 套件回归**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders`
|
||
Expected: PASS
|
||
- [ ] **Step 7: Commit**
|
||
```bash
|
||
git add apps/lib/main.dart apps/lib/core/notifications/reminder_notification_callbacks.dart apps/lib/core/notifications/local_notification_service.dart apps/test/features/calendar/reminders/reminder_notification_callbacks_test.dart
|
||
git commit -m "feat: wire reminder callbacks for foreground and background"
|
||
```
|
||
|
||
### Task 8: Android 平台配置可执行校验(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/platform/android_manifest_notification_action_test.dart`
|
||
- Modify: `apps/android/app/src/main/AndroidManifest.xml`
|
||
|
||
- [ ] **Step 1: 写失败测试(缺 ActionBroadcastReceiver)**
|
||
```dart
|
||
test('android manifest contains ActionBroadcastReceiver', () async {});
|
||
```
|
||
- [ ] **Step 2: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/platform/android_manifest_notification_action_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 3: 增加 receiver 配置**
|
||
- [ ] **Step 4: 跑通过测试**
|
||
Run (workdir=`apps`): `flutter test test/platform/android_manifest_notification_action_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add apps/android/app/src/main/AndroidManifest.xml apps/test/platform/android_manifest_notification_action_test.dart
|
||
git commit -m "fix: register android action receiver for reminder notifications"
|
||
```
|
||
|
||
### Task 9: iOS 平台配置可执行校验(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/platform/ios_app_delegate_notification_callback_test.dart`
|
||
- Modify: `apps/ios/Runner/AppDelegate.swift`
|
||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(registrant callback)**
|
||
```dart
|
||
test('ios app delegate registers flutter local notifications callback', () async {});
|
||
```
|
||
- [ ] **Step 2: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/platform/ios_app_delegate_notification_callback_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 3: 实现 callback 注册 + category version bump (`calendar_reminder_v2`)**
|
||
- [ ] **Step 4: 跑通过测试与 reminders 回归**
|
||
Run (workdir=`apps`): `flutter test test/platform/ios_app_delegate_notification_callback_test.dart`
|
||
Expected: PASS
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders`
|
||
Expected: PASS
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add apps/ios/Runner/AppDelegate.swift apps/lib/core/notifications/local_notification_service.dart apps/test/platform/ios_app_delegate_notification_callback_test.dart
|
||
git commit -m "fix: enable ios reminder action handling in background"
|
||
```
|
||
|
||
### Task 10: Android 13+ 权限降级与埋点(TDD)
|
||
|
||
**Files:**
|
||
- Create: `apps/test/features/calendar/reminders/reminder_permission_fallback_test.dart`
|
||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||
|
||
- [ ] **Step 1: 写失败测试(未授权降级到应用内)**
|
||
```dart
|
||
test('fallbacks to in-app path when notifications permission denied', () async {});
|
||
```
|
||
- [ ] **Step 2: 跑失败测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_permission_fallback_test.dart`
|
||
Expected: FAIL
|
||
- [ ] **Step 3: 最小实现权限检查 + 降级埋点**
|
||
- [ ] **Step 3: 最小实现权限检查 + 降级埋点**
|
||
```text
|
||
埋点字段至少包含:actionExecutionId、permissionState、appLifecycleState、platform
|
||
```
|
||
- [ ] **Step 4: 跑通过测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders/reminder_permission_fallback_test.dart`
|
||
Expected: PASS
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add apps/lib/core/notifications/local_notification_service.dart apps/test/features/calendar/reminders/reminder_permission_fallback_test.dart
|
||
git commit -m "feat: add reminder permission fallback path and telemetry"
|
||
```
|
||
|
||
### Task 11: 旧代码清单与即时清理
|
||
|
||
**Files:**
|
||
- Create: `docs/todo/calendar-reminder-migration-checklist.md`
|
||
- Modify/Delete: `apps/lib/**`, `apps/test/**`(以清单为准)
|
||
|
||
- [ ] **Step 1: 建立迁移清单(文件/符号/决策/责任人)**
|
||
- [ ] **Step 2: 清理前扫描**
|
||
Run: `rg "calendar_reminder_actions_v1|ReminderAction.cancel|_oldReminderEntry|_legacyReminderRoute" apps/lib apps/test`
|
||
Expected: 输出待清理命中(仅代码引用,不含注释/文档示例)
|
||
- [ ] **Step 3: 删除无用旧代码、无效测试、旧 fixture**
|
||
- [ ] **Step 4: 清理后扫描**
|
||
Run: `rg "calendar_reminder_actions_v1|ReminderAction.cancel|_oldReminderEntry|_legacyReminderRoute" apps/lib apps/test`
|
||
Expected: no matches(注释/文档示例允许存在需在清单标注)
|
||
- [ ] **Step 5: Commit**
|
||
```bash
|
||
git add docs/todo/calendar-reminder-migration-checklist.md apps/lib apps/test
|
||
git commit -m "refactor: remove obsolete reminder paths after migration"
|
||
```
|
||
|
||
### Task 12: 最终验证与交付
|
||
|
||
**Files:**
|
||
- Verify only
|
||
|
||
- [ ] **Step 1: reminders 全量测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar/reminders`
|
||
Expected: PASS
|
||
- [ ] **Step 2: calendar 相关测试**
|
||
Run (workdir=`apps`): `flutter test test/features/calendar`
|
||
Expected: PASS
|
||
- [ ] **Step 3: 手工矩阵 6/6 验证**
|
||
```text
|
||
Android: 前台/后台/杀进程
|
||
iOS: 前台/后台锁屏/升级后
|
||
```
|
||
- [ ] **Step 4: 输出证据与指标**
|
||
```text
|
||
命令结果、回放日志、重试日志、重复执行率=0(按 actionExecutionId 统计)
|
||
```
|
||
|
||
---
|
||
|
||
## Execution Notes
|
||
|
||
- 任务顺序不可打乱:协议 -> 动作语义 -> 幂等 -> 回放 -> 桥接 -> UI -> 平台 -> 清理。
|
||
- 每个任务只做最小改动并回归对应测试。
|
||
- 视觉组件严格使用 `apps/lib/core/theme/design_tokens.dart`。
|
||
- 若实现中与 spec 冲突,先改 spec/协议再继续写代码。
|