docs(04): create phase plan

This commit is contained in:
2026-03-16 14:43:37 +01:00
parent 7a2da5f4b8
commit abc56f032f
4 changed files with 758 additions and 2 deletions

View File

@@ -79,7 +79,11 @@ Plans:
2. User can enable or disable notifications from the Settings tab, and the change takes effect immediately 2. User can enable or disable notifications from the Settings tab, and the change takes effect immediately
3. Notifications are correctly rescheduled after device reboot (RECEIVE_BOOT_COMPLETED receiver active) 3. Notifications are correctly rescheduled after device reboot (RECEIVE_BOOT_COMPLETED receiver active)
4. On Android API 33+, the app requests POST_NOTIFICATIONS permission at the appropriate moment and degrades gracefully if denied 4. On Android API 33+, the app requests POST_NOTIFICATIONS permission at the appropriate moment and degrades gracefully if denied
**Plans**: TBD **Plans**: 3 plans
Plans:
- [ ] 04-01-PLAN.md — Infrastructure: packages, Android config, NotificationService, NotificationSettingsNotifier, DAO queries, timezone init, tests
- [ ] 04-02-PLAN.md — Settings UI: Benachrichtigungen section with toggle, time picker, permission flow, scheduling wiring, tests
- [ ] 04-03-PLAN.md — Verification gate: dart analyze + full test suite + requirement confirmation
## Progress ## Progress
@@ -93,4 +97,4 @@ Note: Phase 4 depends on Phase 2 (needs scheduling data) but can be developed in
| 1. Foundation | 2/2 | Complete | 2026-03-15 | | 1. Foundation | 2/2 | Complete | 2026-03-15 |
| 2. Rooms and Tasks | 5/5 | Complete | 2026-03-15 | | 2. Rooms and Tasks | 5/5 | Complete | 2026-03-15 |
| 3. Daily Plan and Cleanliness | 3/3 | Complete | 2026-03-16 | | 3. Daily Plan and Cleanliness | 3/3 | Complete | 2026-03-16 |
| 4. Notifications | 0/TBD | Not started | - | | 4. Notifications | 0/3 | In progress | - |

View File

@@ -0,0 +1,339 @@
---
phase: 04-notifications
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- pubspec.yaml
- android/app/build.gradle.kts
- android/app/src/main/AndroidManifest.xml
- lib/main.dart
- lib/core/notifications/notification_service.dart
- lib/core/notifications/notification_settings_notifier.dart
- lib/core/notifications/notification_settings_notifier.g.dart
- lib/features/home/data/daily_plan_dao.dart
- lib/features/home/data/daily_plan_dao.g.dart
- lib/l10n/app_de.arb
- test/core/notifications/notification_service_test.dart
- test/core/notifications/notification_settings_notifier_test.dart
autonomous: true
requirements:
- NOTF-01
- NOTF-02
must_haves:
truths:
- "NotificationService can schedule a daily notification at a given TimeOfDay"
- "NotificationService can cancel all scheduled notifications"
- "NotificationService can request POST_NOTIFICATIONS permission"
- "NotificationSettingsNotifier persists enabled boolean and TimeOfDay to SharedPreferences"
- "NotificationSettingsNotifier loads persisted values on build"
- "DailyPlanDao can return a one-shot count of overdue + today tasks"
- "Timezone is initialized before any notification scheduling"
- "Android build compiles with core library desugaring enabled"
- "AndroidManifest has POST_NOTIFICATIONS permission, RECEIVE_BOOT_COMPLETED permission, and boot receiver"
artifacts:
- path: "lib/core/notifications/notification_service.dart"
provides: "Singleton wrapper around FlutterLocalNotificationsPlugin"
exports: ["NotificationService"]
- path: "lib/core/notifications/notification_settings_notifier.dart"
provides: "Riverpod notifier for notification enabled + time"
exports: ["NotificationSettings", "NotificationSettingsNotifier"]
- path: "test/core/notifications/notification_service_test.dart"
provides: "Unit tests for scheduling, cancel, permission"
- path: "test/core/notifications/notification_settings_notifier_test.dart"
provides: "Unit tests for persistence and state management"
key_links:
- from: "lib/core/notifications/notification_service.dart"
to: "flutter_local_notifications"
via: "FlutterLocalNotificationsPlugin"
pattern: "FlutterLocalNotificationsPlugin"
- from: "lib/core/notifications/notification_settings_notifier.dart"
to: "shared_preferences"
via: "SharedPreferences persistence"
pattern: "SharedPreferences\\.getInstance"
- from: "lib/main.dart"
to: "lib/core/notifications/notification_service.dart"
via: "timezone init + service initialize"
pattern: "NotificationService.*initialize"
---
<objective>
Install notification packages, configure Android build system, create the NotificationService singleton and NotificationSettingsNotifier provider, add one-shot DAO query for task counts, initialize timezone in main.dart, add ARB strings, and write unit tests.
Purpose: Establish the complete notification infrastructure so Plan 02 can wire it into the Settings UI.
Output: Working notification service and settings notifier with full test coverage. Android build configuration complete.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-notifications/04-CONTEXT.md
@.planning/phases/04-notifications/04-RESEARCH.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From lib/core/theme/theme_provider.dart (pattern to follow for notifier):
```dart
@riverpod
class ThemeNotifier extends _$ThemeNotifier {
@override
ThemeMode build() {
_loadPersistedThemeMode();
return ThemeMode.system; // sync default, then async load overrides
}
Future<void> setThemeMode(ThemeMode mode) async { ... }
}
```
From lib/features/home/data/daily_plan_dao.dart (DAO to extend):
```dart
@DriftAccessor(tables: [Tasks, Rooms, TaskCompletions])
class DailyPlanDao extends DatabaseAccessor<AppDatabase> with _$DailyPlanDaoMixin {
DailyPlanDao(super.attachedDatabase);
Stream<List<TaskWithRoom>> watchAllTasksWithRoomName() { ... }
Stream<int> watchCompletionsToday({DateTime? today}) { ... }
}
```
From lib/main.dart (entry point to modify):
```dart
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: App()));
}
```
From lib/core/router/router.dart (top-level GoRouter for notification tap):
```dart
final router = GoRouter(
initialLocation: '/',
routes: [ StatefulShellRoute.indexedStack(...) ],
);
```
From lib/l10n/app_de.arb (localization file, 92 existing keys):
Last key: "dailyPlanNoTasks": "Noch keine Aufgaben angelegt"
From android/app/build.gradle.kts:
```kotlin
android {
compileSdk = flutter.compileSdkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
```
From android/app/src/main/AndroidManifest.xml:
No notification-related entries exist yet. Only standard Flutter activity + meta-data.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Android config, packages, NotificationService, timezone init, DAO query, ARB strings</name>
<files>
pubspec.yaml,
android/app/build.gradle.kts,
android/app/src/main/AndroidManifest.xml,
lib/main.dart,
lib/core/notifications/notification_service.dart,
lib/features/home/data/daily_plan_dao.dart,
lib/features/home/data/daily_plan_dao.g.dart,
lib/l10n/app_de.arb
</files>
<action>
1. **Add packages** to pubspec.yaml dependencies:
- `flutter_local_notifications: ^21.0.0`
- `timezone: ^0.9.4`
- `flutter_timezone: ^1.0.8`
Run `flutter pub get`.
2. **Configure Android build** in `android/app/build.gradle.kts`:
- Set `compileSdk = 35` (explicit, replacing `flutter.compileSdkVersion`)
- Add `isCoreLibraryDesugaringEnabled = true` inside `compileOptions`
- Add `coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")` in `dependencies` block
3. **Configure AndroidManifest.xml** — add inside `<manifest>` (outside `<application>`):
- `<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>`
- `<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>`
Add inside `<application>`:
- `ScheduledNotificationReceiver` with `android:exported="false"`
- `ScheduledNotificationBootReceiver` with `android:exported="true"` and intent-filter for BOOT_COMPLETED, MY_PACKAGE_REPLACED, QUICKBOOT_POWERON, HTC QUICKBOOT_POWERON
Use the exact XML from RESEARCH.md Pattern section.
4. **Create NotificationService** at `lib/core/notifications/notification_service.dart`:
- Singleton pattern (factory constructor + static `_instance`)
- `final _plugin = FlutterLocalNotificationsPlugin();`
- `Future<void> initialize()`: AndroidInitializationSettings with `@mipmap/ic_launcher`, call `_plugin.initialize(settings, onDidReceiveNotificationResponse: _onTap)`
- `Future<bool> requestPermission()`: resolve Android implementation, call `requestNotificationsPermission()`, return `granted ?? false`
- `Future<void> scheduleDailyNotification({required TimeOfDay time, required String title, required String body})`:
- Call `_plugin.cancelAll()` first
- Compute `_nextInstanceOf(time)` as TZDateTime
- AndroidNotificationDetails: channelId `'daily_summary'`, channelName `'Tagliche Zusammenfassung'`, channelDescription `'Tagliche Aufgaben-Erinnerung'`, importance default, priority default
- Call `_plugin.zonedSchedule(0, title: title, body: body, scheduledDate: scheduledDate, details, androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, matchDateTimeComponents: DateTimeComponents.time)`
- `Future<void> cancelAll()`: delegates to `_plugin.cancelAll()`
- `tz.TZDateTime _nextInstanceOf(TimeOfDay time)`: compute next occurrence (today if in future, tomorrow otherwise)
- `static void _onTap(NotificationResponse response)`: no-op for now (Plan 02 wires navigation)
5. **Add one-shot DAO query** to `lib/features/home/data/daily_plan_dao.dart`:
```dart
/// One-shot count of overdue + today tasks (for notification body).
Future<int> getOverdueAndTodayTaskCount({DateTime? today}) async {
final now = today ?? DateTime.now();
final endOfToday = DateTime(now.year, now.month, now.day + 1);
final result = await (selectOnly(tasks)
..addColumns([tasks.id.count()])
..where(tasks.nextDueDate.isSmallerThanValue(endOfToday)))
.getSingle();
return result.read(tasks.id.count()) ?? 0;
}
/// One-shot count of overdue tasks only (for notification body split).
Future<int> getOverdueTaskCount({DateTime? today}) async {
final now = today ?? DateTime.now();
final startOfToday = DateTime(now.year, now.month, now.day);
final result = await (selectOnly(tasks)
..addColumns([tasks.id.count()])
..where(tasks.nextDueDate.isSmallerThanValue(startOfToday)))
.getSingle();
return result.read(tasks.id.count()) ?? 0;
}
```
Run `dart run build_runner build --delete-conflicting-outputs` to regenerate DAO.
6. **Initialize timezone in main.dart** — before `NotificationService().initialize()`:
```dart
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:flutter_timezone/flutter_timezone.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
tz.initializeTimeZones();
final timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName));
await NotificationService().initialize();
runApp(const ProviderScope(child: App()));
}
```
7. **Add ARB strings** to `lib/l10n/app_de.arb`:
- `settingsSectionNotifications`: "Benachrichtigungen"
- `notificationsEnabledLabel`: "Tägliche Erinnerung"
- `notificationsTimeLabel`: "Uhrzeit"
- `notificationsPermissionDeniedHint`: "Benachrichtigungen sind in den Systemeinstellungen deaktiviert. Tippe hier, um sie zu aktivieren."
- `notificationTitle`: "Dein Tagesplan"
- `notificationBody`: "{count} Aufgaben fällig" with `@notificationBody` placeholder `count: int`
- `notificationBodyWithOverdue`: "{count} Aufgaben fällig ({overdue} überfällig)" with `@notificationBodyWithOverdue` placeholders `count: int, overdue: int`
8. Run `flutter pub get` and `flutter test` to confirm no regressions (expect 72 existing tests to pass).
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test</automated>
</verify>
<done>
- flutter_local_notifications, timezone, flutter_timezone in pubspec.yaml
- build.gradle.kts has compileSdk=35, desugaring enabled, desugar_jdk_libs dependency
- AndroidManifest has POST_NOTIFICATIONS, RECEIVE_BOOT_COMPLETED, both receivers
- NotificationService exists with initialize, requestPermission, scheduleDailyNotification, cancelAll
- DailyPlanDao has getOverdueAndTodayTaskCount and getOverdueTaskCount one-shot queries
- main.dart initializes timezone and notification service before runApp
- ARB file has 7 new notification-related keys
- All 72 existing tests still pass
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: NotificationSettingsNotifier and unit tests</name>
<files>
lib/core/notifications/notification_settings_notifier.dart,
lib/core/notifications/notification_settings_notifier.g.dart,
test/core/notifications/notification_settings_notifier_test.dart,
test/core/notifications/notification_service_test.dart
</files>
<behavior>
- Test: NotificationSettingsNotifier build() returns default state (enabled=false, time=07:00)
- Test: setEnabled(true) updates state.enabled to true and persists to SharedPreferences
- Test: setEnabled(false) updates state.enabled to false and persists to SharedPreferences
- Test: setTime(TimeOfDay(hour: 9, minute: 30)) updates state.time and persists hour+minute to SharedPreferences
- Test: After _load() with existing prefs (enabled=true, hour=8, minute=15), state reflects persisted values
- Test: NotificationService._nextInstanceOf returns today if time is in the future
- Test: NotificationService._nextInstanceOf returns tomorrow if time has passed
</behavior>
<action>
1. **Create NotificationSettingsNotifier** at `lib/core/notifications/notification_settings_notifier.dart`:
- Use `@Riverpod(keepAlive: true)` annotation (NOT plain `@riverpod`) to survive tab switches
- `class NotificationSettings { final bool enabled; final TimeOfDay time; const NotificationSettings({required this.enabled, required this.time}); }`
- `class NotificationSettingsNotifier extends _$NotificationSettingsNotifier`
- `build()` returns `NotificationSettings(enabled: false, time: TimeOfDay(hour: 7, minute: 0))` synchronously, then calls `_load()` which async reads SharedPreferences and updates state
- `Future<void> setEnabled(bool enabled)`: update state, persist `notifications_enabled` bool
- `Future<void> setTime(TimeOfDay time)`: update state, persist `notifications_hour` int and `notifications_minute` int
- Follow exact pattern from ThemeNotifier: sync return default, async load overrides state
- Add `part 'notification_settings_notifier.g.dart';`
2. Run `dart run build_runner build --delete-conflicting-outputs` to generate `.g.dart`.
3. **Write tests** at `test/core/notifications/notification_settings_notifier_test.dart`:
- Use `SharedPreferences.setMockInitialValues({})` for clean state
- Use `SharedPreferences.setMockInitialValues({'notifications_enabled': true, 'notifications_hour': 8, 'notifications_minute': 15})` for pre-existing state
- Create a `ProviderContainer` with the notifier, verify default state, call `setEnabled`/`setTime`, verify state updates and SharedPreferences values
4. **Write tests** at `test/core/notifications/notification_service_test.dart`:
- Test `_nextInstanceOf` logic by extracting it to a `@visibleForTesting` static method or by testing `scheduleDailyNotification` with a mock plugin
- Since `FlutterLocalNotificationsPlugin` dispatches to native and cannot be truly unit-tested, focus tests on:
a. `_nextInstanceOf` returns correct TZDateTime (make it a package-private or `@visibleForTesting` method)
b. Verify the service can be instantiated (singleton pattern)
- Initialize timezone in test setUp: `tz.initializeTimeZones(); tz.setLocalLocation(tz.getLocation('Europe/Berlin'));`
5. Run `flutter test test/core/notifications/` to confirm new tests pass.
6. Run `flutter test` to confirm all tests pass (72 existing + new).
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/core/notifications/</automated>
</verify>
<done>
- NotificationSettingsNotifier with @Riverpod(keepAlive: true) created and generated
- NotificationSettings data class with enabled + time fields
- SharedPreferences persistence for enabled, hour, minute
- Unit tests for default state, setEnabled, setTime, persistence load
- Unit tests for _nextInstanceOf timezone logic
- All tests pass including existing 72
</done>
</task>
</tasks>
<verification>
- `flutter test` — all tests pass (72 existing + new notification tests)
- `dart analyze --fatal-infos` — no warnings or errors
- `grep -r "flutter_local_notifications" pubspec.yaml` — package present
- `grep -r "isCoreLibraryDesugaringEnabled" android/app/build.gradle.kts` — desugaring enabled
- `grep -r "POST_NOTIFICATIONS" android/app/src/main/AndroidManifest.xml` — permission present
- `grep -r "RECEIVE_BOOT_COMPLETED" android/app/src/main/AndroidManifest.xml` — permission present
- `grep -r "ScheduledNotificationBootReceiver" android/app/src/main/AndroidManifest.xml` — receiver present
</verification>
<success_criteria>
- NotificationService singleton with initialize, requestPermission, scheduleDailyNotification, cancelAll
- NotificationSettingsNotifier persists enabled + time to SharedPreferences
- DailyPlanDao has one-shot overdue+today count queries
- Android build configured for flutter_local_notifications v21
- Timezone initialized in main.dart
- All tests pass, dart analyze clean
</success_criteria>
<output>
After completion, create `.planning/phases/04-notifications/04-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,317 @@
---
phase: 04-notifications
plan: 02
type: execute
wave: 2
depends_on:
- 04-01
files_modified:
- lib/features/settings/presentation/settings_screen.dart
- lib/core/router/router.dart
- lib/core/notifications/notification_service.dart
- test/features/settings/settings_screen_test.dart
autonomous: true
requirements:
- NOTF-01
- NOTF-02
must_haves:
truths:
- "Settings screen shows a Benachrichtigungen section between Darstellung and Uber"
- "SwitchListTile toggles notification enabled/disabled"
- "When toggle is ON, time picker row appears below with progressive disclosure animation"
- "When toggle is OFF, time picker row is hidden"
- "Tapping time row opens Material 3 showTimePicker dialog"
- "Toggling ON requests POST_NOTIFICATIONS permission on Android 13+"
- "If permission denied, toggle reverts to OFF"
- "If permanently denied, user is guided to system notification settings"
- "When enabled + time set, daily notification is scheduled with correct body from DAO query"
- "Skip notification scheduling when task count is 0"
- "Notification body shows overdue count only when overdue > 0"
- "Tapping notification navigates to Home tab"
artifacts:
- path: "lib/features/settings/presentation/settings_screen.dart"
provides: "Benachrichtigungen section with toggle and time picker"
contains: "SwitchListTile"
- path: "test/features/settings/settings_screen_test.dart"
provides: "Widget tests for notification settings UI"
key_links:
- from: "lib/features/settings/presentation/settings_screen.dart"
to: "lib/core/notifications/notification_settings_notifier.dart"
via: "ref.watch(notificationSettingsNotifierProvider)"
pattern: "notificationSettingsNotifierProvider"
- from: "lib/features/settings/presentation/settings_screen.dart"
to: "lib/core/notifications/notification_service.dart"
via: "NotificationService().scheduleDailyNotification"
pattern: "NotificationService.*schedule"
- from: "lib/core/router/router.dart"
to: "lib/core/notifications/notification_service.dart"
via: "notification tap navigates to /"
pattern: "router\\.go\\('/'\\)"
---
<objective>
Wire the notification infrastructure into the Settings UI with permission flow, add notification scheduling on toggle/time change, implement notification tap navigation, and write widget tests.
Purpose: Complete the user-facing notification feature — users can enable notifications, pick a time, and receive daily task summaries.
Output: Fully functional notification settings with permission handling, scheduling, and navigation.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-notifications/04-CONTEXT.md
@.planning/phases/04-notifications/04-RESEARCH.md
@.planning/phases/04-notifications/04-01-SUMMARY.md
<interfaces>
<!-- Contracts from Plan 01 that this plan consumes -->
From lib/core/notifications/notification_service.dart (created in Plan 01):
```dart
class NotificationService {
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
Future<void> initialize() async { ... }
Future<bool> requestPermission() async { ... }
Future<void> scheduleDailyNotification({
required TimeOfDay time,
required String title,
required String body,
}) async { ... }
Future<void> cancelAll() async { ... }
}
```
From lib/core/notifications/notification_settings_notifier.dart (created in Plan 01):
```dart
class NotificationSettings {
final bool enabled;
final TimeOfDay time;
const NotificationSettings({required this.enabled, required this.time});
}
@Riverpod(keepAlive: true)
class NotificationSettingsNotifier extends _$NotificationSettingsNotifier {
NotificationSettings build() { ... }
Future<void> setEnabled(bool enabled) async { ... }
Future<void> setTime(TimeOfDay time) async { ... }
}
// Generated provider: notificationSettingsNotifierProvider
```
From lib/features/home/data/daily_plan_dao.dart (extended in Plan 01):
```dart
Future<int> getOverdueAndTodayTaskCount({DateTime? today}) async { ... }
Future<int> getOverdueTaskCount({DateTime? today}) async { ... }
```
From lib/features/settings/presentation/settings_screen.dart (existing):
```dart
class SettingsScreen extends ConsumerWidget {
// ListView with: Darstellung section, Divider, Uber section
}
```
From lib/core/router/router.dart (existing):
```dart
final router = GoRouter(initialLocation: '/', routes: [ ... ]);
```
From lib/l10n/app_de.arb (notification strings from Plan 01):
- settingsSectionNotifications, notificationsEnabledLabel, notificationsTimeLabel
- notificationsPermissionDeniedHint
- notificationTitle, notificationBody, notificationBodyWithOverdue
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Settings UI with Benachrichtigungen section, permission flow, and notification scheduling</name>
<files>
lib/features/settings/presentation/settings_screen.dart,
lib/core/router/router.dart,
lib/core/notifications/notification_service.dart
</files>
<action>
1. **Modify SettingsScreen** (`lib/features/settings/presentation/settings_screen.dart`):
- Change from `ConsumerWidget` to `ConsumerStatefulWidget` (needed for async permission/scheduling logic in callbacks)
- Add `ref.watch(notificationSettingsNotifierProvider)` to get current `NotificationSettings`
- Insert new section BETWEEN the Darstellung Divider and the Uber section header:
```
const Divider(indent: 16, endIndent: 16, height: 32),
// Section 2: Notifications (Benachrichtigungen)
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Text(
l10n.settingsSectionNotifications,
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.primary,
),
),
),
SwitchListTile(
title: Text(l10n.notificationsEnabledLabel),
value: notificationSettings.enabled,
onChanged: (value) => _onNotificationToggle(value),
),
// Progressive disclosure: time picker only when enabled
AnimatedSize(
duration: const Duration(milliseconds: 200),
child: notificationSettings.enabled
? ListTile(
title: Text(l10n.notificationsTimeLabel),
trailing: Text(notificationSettings.time.format(context)),
onTap: () => _onPickTime(),
)
: const SizedBox.shrink(),
),
const Divider(indent: 16, endIndent: 16, height: 32),
// Section 3: About (Uber) — existing code, unchanged
```
2. **Implement `_onNotificationToggle(bool value)`**:
- If `value == true` (enabling):
a. Call `NotificationService().requestPermission()` — await result
b. If `granted == false`: check if permanently denied. On Android, this means `shouldShowRequestRationale` returns false after denial. Since we don't have `permission_handler`, use a simpler approach: if `requestPermission()` returns false, show a SnackBar with `l10n.notificationsPermissionDeniedHint` and an action that calls `openAppSettings()` (import `flutter_local_notifications` for this, or use `AppSettings.openAppSettings()`). Actually, the simplest approach: if permission denied, show a SnackBar with the hint text. Do NOT change the toggle to ON. Return early.
c. If `granted == true`: call `ref.read(notificationSettingsNotifierProvider.notifier).setEnabled(true)`, then schedule notification via `_scheduleNotification()`
- If `value == false` (disabling):
a. Call `ref.read(notificationSettingsNotifierProvider.notifier).setEnabled(false)`
b. Call `NotificationService().cancelAll()`
3. **Implement `_scheduleNotification()`** helper:
- Get the database instance from the Riverpod container: `ref.read(appDatabaseProvider)` (or access DailyPlanDao directly — check how other screens access the database and follow that pattern)
- Query `DailyPlanDao(db).getOverdueAndTodayTaskCount()` for total count
- Query `DailyPlanDao(db).getOverdueTaskCount()` for overdue count
- If total count == 0: call `NotificationService().cancelAll()` and return (skip-on-zero per CONTEXT.md)
- Build notification body:
- If overdue > 0: use `l10n.notificationBodyWithOverdue(total, overdue)`
- If overdue == 0: use `l10n.notificationBody(total)`
- Title: `l10n.notificationTitle` (which is "Dein Tagesplan")
- Call `NotificationService().scheduleDailyNotification(time: settings.time, title: title, body: body)`
4. **Implement `_onPickTime()`**:
- Call `showTimePicker(context: context, initialTime: currentSettings.time, initialEntryMode: TimePickerEntryMode.dial)`
- If picked is not null: call `ref.read(notificationSettingsNotifierProvider.notifier).setTime(picked)`, then call `_scheduleNotification()` to reschedule with new time
5. **Wire notification tap navigation** in `lib/core/notifications/notification_service.dart`:
- Update `_onTap` to use the top-level `router` instance from `lib/core/router/router.dart`:
```dart
import 'package:household_keeper/core/router/router.dart';
static void _onTap(NotificationResponse response) {
router.go('/');
}
```
- This works because `router` is a top-level `final` in router.dart, accessible without BuildContext.
6. **Handle permanently denied state** (Claude's discretion):
- Use a simple approach: if `requestPermission()` returns false AND the toggle was tapped:
- First denial: just show SnackBar with hint text
- Track denial in SharedPreferences (`notifications_permission_denied_once` bool)
- If previously denied and denied again: show SnackBar with action button "Einstellungen offnen" that navigates to system notification settings via `AndroidFlutterLocalNotificationsPlugin`'s `openNotificationSettings()` method or Android intent
- Alternatively (simpler): always show the same SnackBar with the hint text on denial. If the user taps it, attempt to open system settings. This avoids tracking denial state.
- Pick the simpler approach: SnackBar with `notificationsPermissionDeniedHint` text. No tracking needed. The SnackBar message already says "Tippe hier, um sie zu aktivieren" — make the SnackBar action open app notification settings.
7. Run `dart analyze --fatal-infos` to ensure no warnings.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos</automated>
</verify>
<done>
- Settings screen shows Benachrichtigungen section between Darstellung and Uber
- SwitchListTile toggles notification on/off
- Time picker row with AnimatedSize progressive disclosure
- showTimePicker dialog on time row tap
- Permission requested on toggle ON (Android 13+)
- Toggle reverts to OFF on permission denial with SnackBar hint
- Notification scheduled with task count body on enable/time change
- Skip scheduling on zero-task days
- Notification body includes overdue split when overdue > 0
- Tapping notification navigates to Home tab via router.go('/')
- dart analyze clean
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Widget tests for notification settings UI</name>
<files>
test/features/settings/settings_screen_test.dart
</files>
<behavior>
- Test: Settings screen renders Benachrichtigungen section header
- Test: SwitchListTile displays with label "Tagliche Erinnerung" and defaults to OFF
- Test: When notificationSettings.enabled is true, time picker ListTile is visible
- Test: When notificationSettings.enabled is false, time picker ListTile is not visible
- Test: Time picker displays formatted time (e.g. "07:00")
</behavior>
<action>
1. **Create or extend** `test/features/settings/settings_screen_test.dart`:
- Check if file exists; if so, extend it. If not, create it.
- Use provider overrides to inject mock NotificationSettingsNotifier state:
- Override `notificationSettingsNotifierProvider` with a mock/override that returns known state
- Also override `themeProvider` to provide ThemeMode.system (to avoid SharedPreferences issues in tests)
2. **Write widget tests**:
a. "renders Benachrichtigungen section header":
- Pump `SettingsScreen` wrapped in `MaterialApp` with localization delegates + `ProviderScope` with overrides
- Verify `find.text('Benachrichtigungen')` finds one widget
b. "notification toggle defaults to OFF":
- Override notifier with `enabled: false`
- Verify `SwitchListTile` value is false
c. "time picker visible when enabled":
- Override notifier with `enabled: true, time: TimeOfDay(hour: 9, minute: 30)`
- Verify `find.text('09:30')` (or formatted equivalent) finds one widget
- Verify `find.text('Uhrzeit')` finds one widget
d. "time picker hidden when disabled":
- Override notifier with `enabled: false`
- Verify `find.text('Uhrzeit')` finds nothing
3. Run `flutter test test/features/settings/` to confirm tests pass.
4. Run `flutter test` to confirm full suite passes.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/settings/</automated>
</verify>
<done>
- Widget tests exist for notification settings section rendering
- Tests cover: section header present, toggle default OFF, time picker visibility on/off, time display
- All tests pass including full suite
</done>
</task>
</tasks>
<verification>
- `flutter test` — all tests pass (existing + new notification + settings tests)
- `dart analyze --fatal-infos` — no warnings or errors
- Settings screen has Benachrichtigungen section with toggle and conditional time picker
- Permission flow correctly handles grant, deny, and permanently denied
- Notification schedules/cancels based on toggle and time changes
- Notification tap opens Home tab
</verification>
<success_criteria>
- User can toggle notifications on/off from Settings
- Time picker appears only when notifications are enabled
- Permission requested contextually on toggle ON
- Denied permission reverts toggle with helpful SnackBar
- Notification scheduled with task count body (or skipped on zero tasks)
- Notification tap navigates to daily plan
- Widget tests cover key UI states
</success_criteria>
<output>
After completion, create `.planning/phases/04-notifications/04-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,96 @@
---
phase: 04-notifications
plan: 03
type: execute
wave: 3
depends_on:
- 04-02
files_modified: []
autonomous: true
requirements:
- NOTF-01
- NOTF-02
must_haves:
truths:
- "dart analyze --fatal-infos passes with zero issues"
- "All tests pass (72 existing + new notification tests)"
- "NOTF-01 artifacts exist: NotificationService, DAO queries, AndroidManifest permissions, timezone init"
- "NOTF-02 artifacts exist: NotificationSettingsNotifier, Settings UI section, toggle, time picker"
- "Phase 4 requirements are fully addressed"
artifacts: []
key_links: []
---
<objective>
Verify all Phase 4 notification work is complete, tests pass, and code is clean before marking the phase done.
Purpose: Quality gate ensuring NOTF-01 and NOTF-02 are fully implemented before phase completion.
Output: Verification confirmation with test counts and analysis results.
</objective>
<execution_context>
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
@/home/jlmak/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-notifications/04-CONTEXT.md
@.planning/phases/04-notifications/04-01-SUMMARY.md
@.planning/phases/04-notifications/04-02-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Run full verification suite</name>
<files></files>
<action>
1. Run `dart analyze --fatal-infos` — must produce zero issues.
2. Run `flutter test` — must produce zero failures. Record total test count.
3. Verify NOTF-01 requirements by checking file existence and content:
- `lib/core/notifications/notification_service.dart` exists with `scheduleDailyNotification`, `requestPermission`, `cancelAll`
- `lib/features/home/data/daily_plan_dao.dart` has `getOverdueAndTodayTaskCount` and `getOverdueTaskCount`
- `android/app/src/main/AndroidManifest.xml` has `POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, `ScheduledNotificationBootReceiver` with `exported="true"`
- `android/app/build.gradle.kts` has `isCoreLibraryDesugaringEnabled = true` and `compileSdk = 35`
- `lib/main.dart` has timezone initialization and `NotificationService().initialize()`
4. Verify NOTF-02 requirements by checking:
- `lib/core/notifications/notification_settings_notifier.dart` exists with `setEnabled`, `setTime`
- `lib/features/settings/presentation/settings_screen.dart` has `SwitchListTile` and `AnimatedSize` for notification section
- `lib/l10n/app_de.arb` has notification-related keys (`settingsSectionNotifications`, `notificationsEnabledLabel`, etc.)
5. Verify notification tap navigation:
- `lib/core/notifications/notification_service.dart` `_onTap` references `router.go('/')`
6. If any issues found, fix them. If all checks pass, record results.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos && flutter test</automated>
</verify>
<done>
- dart analyze: zero issues
- flutter test: all tests pass (72 existing + new notification/settings tests)
- NOTF-01: NotificationService, DAO queries, Android config, timezone init all confirmed present and functional
- NOTF-02: NotificationSettingsNotifier, Settings UI section, toggle, time picker all confirmed present and functional
- Phase 4 verification gate passed
</done>
</task>
</tasks>
<verification>
- `dart analyze --fatal-infos` — zero issues
- `flutter test` — all tests pass
- All NOTF-01 and NOTF-02 artifacts exist and are correctly wired
</verification>
<success_criteria>
- Phase 4 code is clean (no analysis warnings)
- All tests pass
- Both requirements (NOTF-01, NOTF-02) have their artifacts present and correctly implemented
</success_criteria>
<output>
After completion, create `.planning/phases/04-notifications/04-03-SUMMARY.md`
</output>