From 85ca9fbeafc3765e1a10e3daa393f352345a6616 Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Sun, 15 Feb 2026 23:58:12 +0100 Subject: [PATCH] Route reminder notification taps into app UI --- docs/SETUP.md | 1 + docs/progress.md | 21 +++++- .../reminder_notification_intent_bus.dart | 18 +++++ .../reminder_notification_listener.dart | 67 +++++++++++++++++++ ...eminder_scheduler_local_notifications.dart | 21 +++++- .../reminder_scheduler_provider.dart | 19 +++++- lib/main.dart | 5 +- 7 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 lib/features/reminders/scheduling/reminder_notification_intent_bus.dart create mode 100644 lib/features/reminders/scheduling/reminder_notification_listener.dart diff --git a/docs/SETUP.md b/docs/SETUP.md index 2762fda..5015df2 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -70,6 +70,7 @@ Behavior notes: - android/iOS/macOS/windows: scheduled local notifications - settings screen includes `Request Notification Access` action for runtime permission prompts +- tapping a reminder notification opens reminder management UI in-app ## Local Persistence Backend diff --git a/docs/progress.md b/docs/progress.md index cc3e9da..1396121 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -2,6 +2,25 @@ Updated: 2026-02-15 +## Latest Milestone (2026-02-15): Notification Tap Routing + +Implemented initial in-app routing for reminder notification taps: + +- Added notification intent bus: + - `lib/features/reminders/scheduling/reminder_notification_intent_bus.dart` +- Wired local notifications scheduler to emit tap payloads: + - `lib/features/reminders/scheduling/reminder_scheduler_local_notifications.dart` +- Added app-level intent listener wrapper: + - `lib/features/reminders/scheduling/reminder_notification_listener.dart` + - opens reminder management screen when a reminder notification is tapped +- Mounted listener in authenticated app shell path: + - `lib/main.dart` + +Validation for this milestone: + +- `flutter analyze` -> pass +- `flutter test` -> pass + ## Latest Milestone (2026-02-15): Sync Rejection Repair Navigation Added direct repair navigation from rejected sync items: @@ -242,7 +261,7 @@ Validation for this milestone: 1. Reminder delivery runtime integration: - implemented for Android/iOS/macOS/windows with web/linux safe fallback. - permission UX implemented in settings. - - future enhancement: direct notification tap routing. + - notification tap routing implemented for reminder screen handoff. 2. Conflict resolution UX: - reject/requeue + payload inspect + related-screen navigation implemented. - future enhancement: deep-link directly to specific entity edit form. diff --git a/lib/features/reminders/scheduling/reminder_notification_intent_bus.dart b/lib/features/reminders/scheduling/reminder_notification_intent_bus.dart new file mode 100644 index 0000000..5162518 --- /dev/null +++ b/lib/features/reminders/scheduling/reminder_notification_intent_bus.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +/// Broadcasts reminder notification tap intents to the UI layer. +class ReminderNotificationIntentBus { + final StreamController _controller = + StreamController.broadcast(); + + Stream get intents => _controller.stream; + + void emitTap(String reminderId) { + if (reminderId.trim().isEmpty) { + return; + } + _controller.add(reminderId.trim()); + } + + Future close() => _controller.close(); +} diff --git a/lib/features/reminders/scheduling/reminder_notification_listener.dart b/lib/features/reminders/scheduling/reminder_notification_listener.dart new file mode 100644 index 0000000..215694b --- /dev/null +++ b/lib/features/reminders/scheduling/reminder_notification_listener.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:relationship_saver/features/reminders/reminders_view.dart'; +import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart'; + +/// Listens for reminder notification taps and opens reminder management UI. +class ReminderNotificationListener extends ConsumerStatefulWidget { + const ReminderNotificationListener({required this.child, super.key}); + + final Widget child; + + @override + ConsumerState createState() => + _ReminderNotificationListenerState(); +} + +class _ReminderNotificationListenerState + extends ConsumerState { + StreamSubscription? _subscription; + + @override + void initState() { + super.initState(); + _subscription = ref + .read(reminderNotificationIntentBusProvider) + .intents + .listen(_handleIntent); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; + + Future _handleIntent(String reminderId) async { + if (!mounted) { + return; + } + + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + _ReminderNotificationScreen(reminderId: reminderId), + ), + ); + } +} + +class _ReminderNotificationScreen extends StatelessWidget { + const _ReminderNotificationScreen({required this.reminderId}); + + final String reminderId; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Reminder $reminderId')), + body: const RemindersView(), + ); + } +} diff --git a/lib/features/reminders/scheduling/reminder_scheduler_local_notifications.dart b/lib/features/reminders/scheduling/reminder_scheduler_local_notifications.dart index 8c6e501..c4eb160 100644 --- a/lib/features/reminders/scheduling/reminder_scheduler_local_notifications.dart +++ b/lib/features/reminders/scheduling/reminder_scheduler_local_notifications.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:relationship_saver/features/local/local_models.dart'; +import 'package:relationship_saver/features/reminders/scheduling/reminder_notification_intent_bus.dart'; import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart'; import 'package:timezone/timezone.dart' as tz; @@ -14,7 +15,10 @@ class LocalNotificationsReminderScheduler implements ReminderScheduler { LocalNotificationsReminderScheduler({ ReminderNotificationsDriver? driver, DateTime Function()? now, - }) : _driver = driver ?? FlutterReminderNotificationsDriver(), + ReminderNotificationIntentBus? intentBus, + }) : _driver = + driver ?? + FlutterReminderNotificationsDriver(onTapPayload: intentBus?.emitTap), _now = now ?? DateTime.now; final ReminderNotificationsDriver _driver; @@ -122,10 +126,14 @@ abstract class ReminderNotificationsDriver { class FlutterReminderNotificationsDriver implements ReminderNotificationsDriver { - FlutterReminderNotificationsDriver({FlutterLocalNotificationsPlugin? plugin}) - : _plugin = plugin ?? FlutterLocalNotificationsPlugin(); + FlutterReminderNotificationsDriver({ + FlutterLocalNotificationsPlugin? plugin, + void Function(String payload)? onTapPayload, + }) : _plugin = plugin ?? FlutterLocalNotificationsPlugin(), + _onTapPayload = onTapPayload; final FlutterLocalNotificationsPlugin _plugin; + final void Function(String payload)? _onTapPayload; Future? _initializeFuture; @override @@ -204,6 +212,13 @@ class FlutterReminderNotificationsDriver guid: 'ee23c0af-5898-4d96-b599-cf2d7d0d37d7', ), ), + onDidReceiveNotificationResponse: (NotificationResponse response) { + final String? payload = response.payload; + if (payload == null || payload.isEmpty) { + return; + } + _onTapPayload?.call(payload); + }, ); } diff --git a/lib/features/reminders/scheduling/reminder_scheduler_provider.dart b/lib/features/reminders/scheduling/reminder_scheduler_provider.dart index 79c70a3..4e4dd8e 100644 --- a/lib/features/reminders/scheduling/reminder_scheduler_provider.dart +++ b/lib/features/reminders/scheduling/reminder_scheduler_provider.dart @@ -1,13 +1,30 @@ +import 'dart:async'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_config.dart'; +import 'package:relationship_saver/features/reminders/scheduling/reminder_notification_intent_bus.dart'; import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart'; import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_local_notifications.dart'; +/// Broadcasts reminder notification tap intents to interested UI listeners. +final Provider +reminderNotificationIntentBusProvider = Provider( + (Ref ref) { + final ReminderNotificationIntentBus bus = ReminderNotificationIntentBus(); + ref.onDispose(() { + unawaited(bus.close()); + }); + return bus; + }, +); + /// Provides reminder delivery scheduler implementation. final Provider reminderSchedulerProvider = Provider((Ref ref) { if (AppConfig.enableLocalNotifications) { - return LocalNotificationsReminderScheduler(); + return LocalNotificationsReminderScheduler( + intentBus: ref.watch(reminderNotificationIntentBusProvider), + ); } return const NoopReminderScheduler(); }); diff --git a/lib/main.dart b/lib/main.dart index e4ef309..1d9bbcb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/auth/session_controller.dart'; import 'package:relationship_saver/features/auth/sign_in_view.dart'; import 'package:relationship_saver/features/home/app_shell.dart'; +import 'package:relationship_saver/features/reminders/scheduling/reminder_notification_listener.dart'; import 'package:relationship_saver/features/sync/sync_background_runner.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; @@ -28,7 +29,9 @@ class RelationshipSaverApp extends ConsumerWidget { body: sessionState.when( data: (session) => session == null ? const SignInView() - : const SyncBackgroundRunner(child: AppShell()), + : const ReminderNotificationListener( + child: SyncBackgroundRunner(child: AppShell()), + ), loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) => const SignInView(), ),