From abc56f032f59de5c63b5ae62c26d6ce92ef5721b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 14:43:37 +0100 Subject: [PATCH] docs(04): create phase plan --- .planning/ROADMAP.md | 8 +- .../phases/04-notifications/04-01-PLAN.md | 339 ++++++++++++++++++ .../phases/04-notifications/04-02-PLAN.md | 317 ++++++++++++++++ .../phases/04-notifications/04-03-PLAN.md | 96 +++++ 4 files changed, 758 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/04-notifications/04-01-PLAN.md create mode 100644 .planning/phases/04-notifications/04-02-PLAN.md create mode 100644 .planning/phases/04-notifications/04-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 705a49f..0f7b186 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -79,7 +79,11 @@ Plans: 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) 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 @@ -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 | | 2. Rooms and Tasks | 5/5 | Complete | 2026-03-15 | | 3. Daily Plan and Cleanliness | 3/3 | Complete | 2026-03-16 | -| 4. Notifications | 0/TBD | Not started | - | +| 4. Notifications | 0/3 | In progress | - | diff --git a/.planning/phases/04-notifications/04-01-PLAN.md b/.planning/phases/04-notifications/04-01-PLAN.md new file mode 100644 index 0000000..8210c13 --- /dev/null +++ b/.planning/phases/04-notifications/04-01-PLAN.md @@ -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" +--- + + +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. + + + +@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md +@/home/jlmak/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/04-notifications/04-CONTEXT.md +@.planning/phases/04-notifications/04-RESEARCH.md + + + + +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 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 with _$DailyPlanDaoMixin { + DailyPlanDao(super.attachedDatabase); + Stream> watchAllTasksWithRoomName() { ... } + Stream 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. + + + + + + + Task 1: Android config, packages, NotificationService, timezone init, DAO query, ARB strings + + 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 + + + 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 `` (outside ``): + - `` + - `` + Add inside ``: + - `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 initialize()`: AndroidInitializationSettings with `@mipmap/ic_launcher`, call `_plugin.initialize(settings, onDidReceiveNotificationResponse: _onTap)` + - `Future requestPermission()`: resolve Android implementation, call `requestNotificationsPermission()`, return `granted ?? false` + - `Future 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 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 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 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). + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test + + + - 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 + + + + + Task 2: NotificationSettingsNotifier and unit tests + + 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 + + + - 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 + + + 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 setEnabled(bool enabled)`: update state, persist `notifications_enabled` bool + - `Future 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). + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/core/notifications/ + + + - 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 + + + + + + +- `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 + + + +- 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 + + + +After completion, create `.planning/phases/04-notifications/04-01-SUMMARY.md` + diff --git a/.planning/phases/04-notifications/04-02-PLAN.md b/.planning/phases/04-notifications/04-02-PLAN.md new file mode 100644 index 0000000..4625402 --- /dev/null +++ b/.planning/phases/04-notifications/04-02-PLAN.md @@ -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\\('/'\\)" +--- + + +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. + + + +@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md +@/home/jlmak/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +From lib/core/notifications/notification_service.dart (created in Plan 01): +```dart +class NotificationService { + static final NotificationService _instance = NotificationService._internal(); + factory NotificationService() => _instance; + + Future initialize() async { ... } + Future requestPermission() async { ... } + Future scheduleDailyNotification({ + required TimeOfDay time, + required String title, + required String body, + }) async { ... } + Future 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 setEnabled(bool enabled) async { ... } + Future setTime(TimeOfDay time) async { ... } +} +// Generated provider: notificationSettingsNotifierProvider +``` + +From lib/features/home/data/daily_plan_dao.dart (extended in Plan 01): +```dart +Future getOverdueAndTodayTaskCount({DateTime? today}) async { ... } +Future 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 + + + + + + + Task 1: Settings UI with Benachrichtigungen section, permission flow, and notification scheduling + + lib/features/settings/presentation/settings_screen.dart, + lib/core/router/router.dart, + lib/core/notifications/notification_service.dart + + + 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. + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos + + + - 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 + + + + + Task 2: Widget tests for notification settings UI + + test/features/settings/settings_screen_test.dart + + + - 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") + + + 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. + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/settings/ + + + - 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 + + + + + + +- `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 + + + +- 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 + + + +After completion, create `.planning/phases/04-notifications/04-02-SUMMARY.md` + diff --git a/.planning/phases/04-notifications/04-03-PLAN.md b/.planning/phases/04-notifications/04-03-PLAN.md new file mode 100644 index 0000000..753b00c --- /dev/null +++ b/.planning/phases/04-notifications/04-03-PLAN.md @@ -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: [] +--- + + +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. + + + +@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md +@/home/jlmak/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Task 1: Run full verification suite + + + 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. + + + cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && dart analyze --fatal-infos && flutter test + + + - 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 + + + + + + +- `dart analyze --fatal-infos` — zero issues +- `flutter test` — all tests pass +- All NOTF-01 and NOTF-02 artifacts exist and are correctly wired + + + +- Phase 4 code is clean (no analysis warnings) +- All tests pass +- Both requirements (NOTF-01, NOTF-02) have their artifacts present and correctly implemented + + + +After completion, create `.planning/phases/04-notifications/04-03-SUMMARY.md` +