Files
2026-03-16 20:12:01 +01:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-notifications 02 execute 2
04-01
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
true
NOTF-01
NOTF-02
truths artifacts key_links
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
path provides contains
lib/features/settings/presentation/settings_screen.dart Benachrichtigungen section with toggle and time picker SwitchListTile
path provides
test/features/settings/settings_screen_test.dart Widget tests for notification settings UI
from to via pattern
lib/features/settings/presentation/settings_screen.dart lib/core/notifications/notification_settings_notifier.dart ref.watch(notificationSettingsNotifierProvider) notificationSettingsNotifierProvider
from to via pattern
lib/features/settings/presentation/settings_screen.dart lib/core/notifications/notification_service.dart NotificationService().scheduleDailyNotification NotificationService.*schedule
from to via pattern
lib/core/router/router.dart lib/core/notifications/notification_service.dart notification tap navigates to / 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.

<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_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

From lib/core/notifications/notification_service.dart (created in Plan 01):

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):

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):

Future<int> getOverdueAndTodayTaskCount({DateTime? today}) async { ... }
Future<int> getOverdueTaskCount({DateTime? today}) async { ... }

From lib/features/settings/presentation/settings_screen.dart (existing):

class SettingsScreen extends ConsumerWidget {
  // ListView with: Darstellung section, Divider, Uber section
}

From lib/core/router/router.dart (existing):

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

<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>
After completion, create `.planning/phases/04-notifications/04-02-SUMMARY.md`