docs: add reminder overlay implementation plan
This commit is contained in:
@@ -0,0 +1,760 @@
|
||||
# Reminder Overlay Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 简化日历提醒机制,用 ReminderOverlay 统一处理所有用户交互,移除前台/后台判断逻辑。
|
||||
|
||||
**Architecture:** 每条通知独立 payload,点击通知后打开 ReminderOverlay 处理用户操作(完成/稍后提醒)。同分钟多条通知按时间排序依次处理。操作完成后 app 退到后台。iOS 冷启动通过 UserDefaults 传递 payload,Android 使用 full-screen intent。
|
||||
|
||||
**Tech Stack:** Flutter, flutter_local_notifications, Provider/Bloc (状态管理)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 创建 ReminderQueueManager
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/reminder_queue_manager.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/reminder_queue_manager_test.dart`
|
||||
|
||||
**Step 1: 编写测试**
|
||||
|
||||
```dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:social_app/features/calendar/reminders/reminder_queue_manager.dart';
|
||||
import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart';
|
||||
|
||||
void main() {
|
||||
group('ReminderQueueManager', () {
|
||||
test('按点击顺序处理,第一条处理完后处理剩余的按时间排序', () {
|
||||
final manager = ReminderQueueManager();
|
||||
|
||||
final event1 = ReminderPayload(eventId: '1', title: 'Event 1', startAt: DateTime(2026, 3, 20, 10, 1), mode: ReminderPayloadMode.single);
|
||||
final event2 = ReminderPayload(eventId: '2', title: 'Event 2', startAt: DateTime(2026, 3, 20, 10, 2), mode: ReminderPayloadMode.single);
|
||||
final event3 = ReminderPayload(eventId: '3', title: 'Event 3', startAt: DateTime(2026, 3, 20, 10, 3), mode: ReminderPayloadMode.single);
|
||||
|
||||
// 用户点击 event2
|
||||
manager.enqueueFromClick(event2);
|
||||
// 剩余 event1 和 event3 按时间排序: event1 -> event3
|
||||
manager.enqueuePending([event1, event3]);
|
||||
|
||||
expect(manager.currentPayload?.eventId, '2');
|
||||
manager.dequeueCurrent();
|
||||
expect(manager.currentPayload?.eventId, '1');
|
||||
manager.dequeueCurrent();
|
||||
expect(manager.currentPayload?.eventId, '3');
|
||||
manager.dequeueCurrent();
|
||||
expect(manager.isEmpty, true);
|
||||
});
|
||||
|
||||
test('单条通知处理完直接清空', () {
|
||||
final manager = ReminderQueueManager();
|
||||
final event = ReminderPayload(eventId: '1', title: 'Event 1', startAt: DateTime.now(), mode: ReminderPayloadMode.single);
|
||||
|
||||
manager.enqueueFromClick(event);
|
||||
expect(manager.isEmpty, false);
|
||||
manager.dequeueCurrent();
|
||||
expect(manager.isEmpty, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 运行测试验证失败**
|
||||
|
||||
Run: `cd apps && flutter test test/features/calendar/reminders/reminder_queue_manager_test.dart`
|
||||
Expected: FAIL - ReminderQueueManager not defined
|
||||
|
||||
**Step 3: 编写最小实现**
|
||||
|
||||
```dart
|
||||
import 'reminder_payload.dart';
|
||||
|
||||
class ReminderQueueManager {
|
||||
ReminderPayload? _currentPayload;
|
||||
final List<ReminderPayload> _pending = [];
|
||||
|
||||
void enqueueFromClick(ReminderPayload payload) {
|
||||
_currentPayload = payload;
|
||||
}
|
||||
|
||||
void enqueuePending(List<ReminderPayload> payloads) {
|
||||
payloads.sort((a, b) => a.startAt.compareTo(b.startAt));
|
||||
_pending.addAll(payloads);
|
||||
}
|
||||
|
||||
ReminderPayload? get currentPayload => _currentPayload;
|
||||
|
||||
bool get isEmpty => _currentPayload == null && _pending.isEmpty;
|
||||
|
||||
void dequeueCurrent() {
|
||||
_currentPayload = null;
|
||||
if (_pending.isNotEmpty) {
|
||||
_currentPayload = _pending.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_currentPayload = null;
|
||||
_pending.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: 运行测试验证通过**
|
||||
|
||||
Run: `cd apps && flutter test test/features/calendar/reminders/reminder_queue_manager_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders/reminder_queue_manager.dart apps/test/features/calendar/reminders/reminder_queue_manager_test.dart
|
||||
git commit -m "feat(calendar): add ReminderQueueManager for handling multiple notifications"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 创建 IOSNotificationPayloadBridge
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/core/notifications/ios_notification_payload_bridge.dart`
|
||||
- Test: `apps/test/core/notifications/ios_notification_payload_bridge_test.dart`
|
||||
|
||||
**Step 1: 编写测试**
|
||||
|
||||
```dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:social_app/core/notifications/ios_notification_payload_bridge.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
void main() {
|
||||
group('IOSNotificationPayloadBridge', () {
|
||||
test('启动时读取待处理的 notification payload', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'pending_notification_payload': jsonEncode({
|
||||
'eventId': 'evt_123',
|
||||
'title': 'Test Event',
|
||||
'startAt': '2026-03-20T10:00:00Z',
|
||||
'mode': 'single',
|
||||
}),
|
||||
});
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final bridge = IOSNotificationPayloadBridge(prefs);
|
||||
final payload = await bridge.getPendingPayload();
|
||||
|
||||
expect(payload?.eventId, 'evt_123');
|
||||
expect(payload?.title, 'Test Event');
|
||||
});
|
||||
|
||||
test('处理完成后清理 UserDefaults', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'pending_notification_payload': jsonEncode({
|
||||
'eventId': 'evt_123',
|
||||
'title': 'Test Event',
|
||||
'startAt': '2026-03-20T10:00:00Z',
|
||||
'mode': 'single',
|
||||
}),
|
||||
});
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final bridge = IOSNotificationPayloadBridge(prefs);
|
||||
await bridge.clearPendingPayload();
|
||||
|
||||
final remaining = prefs.getString('pending_notification_payload');
|
||||
expect(remaining, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 运行测试验证失败**
|
||||
|
||||
Run: `cd apps && flutter test test/core/notifications/ios_notification_payload_bridge_test.dart`
|
||||
Expected: FAIL - IOSNotificationPayloadBridge not defined
|
||||
|
||||
**Step 3: 编写最小实现**
|
||||
|
||||
```dart
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../features/calendar/reminders/models/reminder_payload.dart';
|
||||
|
||||
class IOSNotificationPayloadBridge {
|
||||
static const String _key = 'pending_notification_payload';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
IOSNotificationPayloadBridge(this._prefs);
|
||||
|
||||
Future<ReminderPayload?> getPendingPayload() async {
|
||||
final raw = _prefs.getString(_key);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final json = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
||||
return ReminderPayload.fromJson(json);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearPendingPayload() async {
|
||||
await _prefs.remove(_key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: 运行测试验证通过**
|
||||
|
||||
Run: `cd apps && flutter test test/core/notifications/ios_notification_payload_bridge_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/notifications/ios_notification_payload_bridge.dart apps/test/core/notifications/ios_notification_payload_bridge_test.dart
|
||||
git commit -m "feat(notifications): add IOSNotificationPayloadBridge for cold start handling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 创建 ReminderOverlay UI 组件
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/ui/reminder_overlay.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/reminder_overlay_test.dart`
|
||||
|
||||
**Step 1: 编写测试**
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:social_app/features/calendar/reminders/ui/reminder_overlay.dart';
|
||||
import 'package:social_app/features/calendar/reminders/reminder_queue_manager.dart';
|
||||
import 'package:social_app/core/notifications/local_notification_service.dart';
|
||||
import 'package:social_app/core/notifications/ios_notification_payload_bridge.dart';
|
||||
import 'package:social_app/features/calendar/data/services/calendar_service.dart';
|
||||
import 'package:social_app/features/calendar/reminders/reminder_queue_manager.dart';
|
||||
import 'package:social_app/features/calendar/reminders/models/reminder_payload.dart';
|
||||
|
||||
void main() {
|
||||
group('ReminderOverlay', () {
|
||||
late ReminderQueueManager queueManager;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
queueManager = ReminderQueueManager();
|
||||
});
|
||||
|
||||
testWidgets('显示日程标题和当前时间', (tester) async {
|
||||
final payload = ReminderPayload(
|
||||
eventId: '1',
|
||||
title: 'Test Meeting',
|
||||
startAt: DateTime(2026, 3, 20, 10, 0),
|
||||
mode: ReminderPayloadMode.single,
|
||||
);
|
||||
queueManager.enqueueFromClick(payload);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ReminderOverlay(
|
||||
queueManager: queueManager,
|
||||
onComplete: () {},
|
||||
onSnooze: (minutes) {},
|
||||
onArchive: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Test Meeting'), findsOneWidget);
|
||||
// 当前时间显示在界面上
|
||||
expect(find.textContaining(':'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('点击完成按钮触发归档', (tester) async {
|
||||
bool archiveCalled = false;
|
||||
final payload = ReminderPayload(
|
||||
eventId: '1',
|
||||
title: 'Test Meeting',
|
||||
startAt: DateTime(2026, 3, 20, 10, 0),
|
||||
mode: ReminderPayloadMode.single,
|
||||
);
|
||||
queueManager.enqueueFromClick(payload);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ReminderOverlay(
|
||||
queueManager: queueManager,
|
||||
onComplete: () {},
|
||||
onSnooze: (minutes) {},
|
||||
onArchive: () => archiveCalled = true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('完成'));
|
||||
await tester.pump();
|
||||
|
||||
expect(archiveCalled, true);
|
||||
});
|
||||
|
||||
testWidgets('点击稍后提醒显示下拉选项', (tester) async {
|
||||
final payload = ReminderPayload(
|
||||
eventId: '1',
|
||||
title: 'Test Meeting',
|
||||
startAt: DateTime(2026, 3, 20, 10, 0),
|
||||
mode: ReminderPayloadMode.single,
|
||||
);
|
||||
queueManager.enqueueFromClick(payload);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ReminderOverlay(
|
||||
queueManager: queueManager,
|
||||
onComplete: () {},
|
||||
onSnooze: (minutes) {},
|
||||
onArchive: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('稍后提醒'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('5 分钟'), findsOneWidget);
|
||||
expect(find.text('15 分钟'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 运行测试验证失败**
|
||||
|
||||
Run: `cd apps && flutter test test/features/calendar/reminders/reminder_overlay_test.dart`
|
||||
Expected: FAIL - ReminderOverlay not defined
|
||||
|
||||
**Step 3: 编写最小实现**
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../reminders/reminder_queue_manager.dart';
|
||||
import '../../reminders/models/reminder_payload.dart';
|
||||
|
||||
class ReminderOverlay extends StatefulWidget {
|
||||
const ReminderOverlay({
|
||||
super.key,
|
||||
required this.queueManager,
|
||||
required this.onComplete,
|
||||
required this.onSnooze,
|
||||
required this.onArchive,
|
||||
});
|
||||
|
||||
final ReminderQueueManager queueManager;
|
||||
final VoidCallback onComplete;
|
||||
final void Function(int minutes) onSnooze;
|
||||
final VoidCallback onArchive;
|
||||
|
||||
@override
|
||||
State<ReminderOverlay> createState() => _ReminderOverlayState();
|
||||
}
|
||||
|
||||
class _ReminderOverlayState extends State<ReminderOverlay> {
|
||||
bool _showSnoozeOptions = false;
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
ReminderPayload? get _currentPayload => widget.queueManager.currentPayload;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideSnoozeOptions();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _hideSnoozeOptions() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
setState(() {
|
||||
_showSnoozeOptions = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _showSnoozeDropdown() {
|
||||
_hideSnoozeOptions();
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
width: 120,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_SnoozeOption(
|
||||
label: '5 分钟',
|
||||
onTap: () {
|
||||
_hideSnoozeOptions();
|
||||
widget.onSnooze(5);
|
||||
},
|
||||
),
|
||||
Divider(height: 1, color: AppColors.borderSecondary),
|
||||
_SnoozeOption(
|
||||
label: '15 分钟',
|
||||
onTap: () {
|
||||
_hideSnoozeOptions();
|
||||
widget.onSnooze(15);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
setState(() {
|
||||
_showSnoozeOptions = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleComplete() {
|
||||
widget.onArchive();
|
||||
widget.queueManager.dequeueCurrent();
|
||||
if (!widget.queueManager.isEmpty) {
|
||||
// 下一条会通过外部状态管理打开新的 overlay
|
||||
}
|
||||
widget.onComplete();
|
||||
}
|
||||
|
||||
void _handleSnooze(int minutes) {
|
||||
widget.onSnooze(minutes);
|
||||
widget.queueManager.dequeueCurrent();
|
||||
if (!widget.queueManager.isEmpty) {
|
||||
// 下一条会通过外部状态管理打开新的 overlay
|
||||
}
|
||||
widget.onComplete();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final payload = _currentPayload;
|
||||
if (payload == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: AppColors.white,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
payload.title,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: AppColors.slate900,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
DateFormat('HH:mm').format(DateTime.now()),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: AppButton(
|
||||
text: '稍后提醒',
|
||||
isOutlined: true,
|
||||
onPressed: _showSnoozeDropdown,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
text: '完成',
|
||||
onPressed: _handleComplete,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SnoozeOption extends StatelessWidget {
|
||||
const _SnoozeOption({
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: 运行测试验证通过**
|
||||
|
||||
Run: `cd apps && flutter test test/features/calendar/reminders/reminder_overlay_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders/ui/reminder_overlay.dart apps/test/features/calendar/reminders/reminder_overlay_test.dart
|
||||
git commit -m "feat(calendar): add ReminderOverlay UI component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 修改 LocalNotificationService - 移除权限降级逻辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||||
|
||||
**Step 1: 阅读现有代码确认移除范围**
|
||||
|
||||
Read: `apps/lib/core/notifications/local_notification_service.dart`
|
||||
|
||||
**Step 2: 移除以下逻辑**
|
||||
|
||||
1. 移除 `_canDeliverSystemNotification` 相关判断
|
||||
2. 移除 `_scheduleInAppFallbackRemindersFrom` 方法
|
||||
3. 移除 `_scheduleInAppFallbackPayload` 方法
|
||||
4. 移除 `_inAppFallbackTimersByEventId` 及相关方法
|
||||
5. 移除 `_trackFallback` 方法
|
||||
6. 移除 `rebuildUpcomingReminders` 中的降级分支
|
||||
|
||||
**Step 3: 验证 flutter analyze 通过**
|
||||
|
||||
Run: `cd apps && flutter analyze lib/core/notifications/local_notification_service.dart`
|
||||
Expected: 无错误
|
||||
|
||||
**Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/notifications/local_notification_service.dart
|
||||
git commit -m "refactor(notifications): remove permission fallback logic"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 修改通知发送逻辑 - 使用原生分组
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||||
|
||||
**Step 1: 添加 threadIdentifier/groupKey 到通知详情**
|
||||
|
||||
在 `_buildNotificationDetails` 方法中:
|
||||
- iOS: 添加 `threadIdentifier: _getThreadIdentifier(fireAt)`
|
||||
- Android: 添加 `groupKey: _getGroupKey(fireAt)`
|
||||
|
||||
**Step 2: 实现分组 key 方法**
|
||||
|
||||
```dart
|
||||
String _getThreadIdentifier(DateTime fireAt) {
|
||||
final bucket = fireAt.millisecondsSinceEpoch ~/ const Duration(minutes: 1).inMilliseconds;
|
||||
return 'calendar_reminder_$bucket';
|
||||
}
|
||||
|
||||
String _getGroupKey(DateTime fireAt) {
|
||||
final bucket = fireAt.millisecondsSinceEpoch ~/ const Duration(minutes: 1).inMilliseconds;
|
||||
return 'com.socialapp.calendar.$bucket';
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: 验证 flutter analyze 通过**
|
||||
|
||||
Run: `cd apps && flutter analyze lib/core/notifications/local_notification_service.dart`
|
||||
Expected: 无错误
|
||||
|
||||
**Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/notifications/local_notification_service.dart
|
||||
git commit -m "feat(notifications): add native notification grouping by time bucket"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 修改 main.dart - 集成 iOS payload bridge
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/main.dart`
|
||||
|
||||
**Step 1: 在 main() 中添加启动时检查**
|
||||
|
||||
在 `runApp` 之前或 app 初始化时:
|
||||
1. 创建 IOSNotificationPayloadBridge 实例
|
||||
2. 调用 `getPendingPayload()`
|
||||
3. 如果有 payload,通过 ReminderQueueManager 处理
|
||||
|
||||
**Step 2: 验证 flutter analyze 通过**
|
||||
|
||||
Run: `cd apps && flutter analyze lib/main.dart`
|
||||
Expected: 无错误
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/lib/main.dart
|
||||
git commit -m "feat(ios): integrate IOSNotificationPayloadBridge for cold start handling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: 删除废弃组件
|
||||
|
||||
**Files:**
|
||||
- Delete: `apps/lib/features/calendar/reminders/reminder_cold_start_queue.dart`
|
||||
- Delete: `apps/lib/features/calendar/reminders/reminder_outbox_store.dart`
|
||||
- Delete: `apps/lib/features/calendar/reminders/reminder_action_dedupe_store.dart`
|
||||
- Delete: `apps/lib/features/calendar/reminders/reminder_overlap_policy.dart`
|
||||
- Delete: `apps/lib/features/calendar/reminders/ui/reminder_foreground_presenter.dart`
|
||||
- Delete: `apps/lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart`
|
||||
- Delete: `apps/lib/features/calendar/reminders/ui/widgets/reminder_action_sheet.dart`
|
||||
|
||||
**Step 1: 删除文件**
|
||||
|
||||
```bash
|
||||
git rm apps/lib/features/calendar/reminders/reminder_cold_start_queue.dart
|
||||
git rm apps/lib/features/calendar/reminders/reminder_outbox_store.dart
|
||||
git rm apps/lib/features/calendar/reminders/reminder_action_dedupe_store.dart
|
||||
git rm apps/lib/features/calendar/reminders/reminder_overlap_policy.dart
|
||||
git rm apps/lib/features/calendar/reminders/ui/reminder_foreground_presenter.dart
|
||||
git rm apps/lib/features/calendar/reminders/ui/reminder_presentation_coordinator.dart
|
||||
git rm apps/lib/features/calendar/reminders/ui/widgets/reminder_action_sheet.dart
|
||||
```
|
||||
|
||||
**Step 2: 删除测试文件**
|
||||
|
||||
```bash
|
||||
git rm apps/test/features/calendar/reminders/reminder_cold_start_queue_test.dart
|
||||
git rm apps/test/features/calendar/reminders/reminder_outbox_store_test.dart
|
||||
git rm apps/test/features/calendar/reminders/reminder_action_dedupe_store_test.dart
|
||||
git rm apps/test/features/calendar/reminders/reminder_overlap_policy_test.dart
|
||||
git rm apps/test/features/calendar/reminders/reminder_foreground_presenter_test.dart
|
||||
git rm apps/test/features/calendar/reminders/reminder_presentation_coordinator_test.dart
|
||||
git rm apps/test/features/calendar/reminders/reminder_action_sheet_test.dart
|
||||
```
|
||||
|
||||
**Step 3: 运行 flutter analyze 验证无引用错误**
|
||||
|
||||
Run: `cd apps && flutter analyze`
|
||||
Expected: 无错误(可能有 deprecated warnings 可以忽略)
|
||||
|
||||
**Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git commit -m "refactor(calendar): remove deprecated reminder components"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: iOS 原生层配置 (AppDelegate.swift)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/ios/Runner/AppDelegate.swift`
|
||||
|
||||
**Step 1: 添加 UserDefaults 写入逻辑**
|
||||
|
||||
在 `userNotificationCenter(_, didReceive, withCompletionHandler)` 中:
|
||||
1. 获取 notification 的 `userInfo`
|
||||
2. 将 payload 写入 `UserDefaults.standard`
|
||||
3. Key: `pending_notification_payload`
|
||||
|
||||
**Step 2: 验证 Xcode build**
|
||||
|
||||
Run: `cd apps && flutter build ios --simulator --no-codesign 2>&1 | head -50`
|
||||
Expected: Build 成功
|
||||
|
||||
**Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/ios/Runner/AppDelegate.swift
|
||||
git commit -m "feat(ios): write notification payload to UserDefaults on cold start"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证清单
|
||||
|
||||
完成所有任务后,运行以下验证:
|
||||
|
||||
```bash
|
||||
# 1. Flutter analyze
|
||||
cd apps && flutter analyze
|
||||
|
||||
# 2. 测试
|
||||
cd apps && flutter test test/features/calendar/reminders/
|
||||
|
||||
# 3. iOS build
|
||||
cd apps && flutter build ios --simulator --no-codesign
|
||||
|
||||
# 4. Android build
|
||||
cd apps && flutter build apk --debug
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plan Complete
|
||||
|
||||
**Plan saved to:** `docs/plans/2026-03-20-reminder-overlay-implementation-plan.md`
|
||||
|
||||
**Two execution options:**
|
||||
|
||||
**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
|
||||
|
||||
**Which approach?**
|
||||
Reference in New Issue
Block a user