--- 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`