Route reminder notification taps into app UI

This commit is contained in:
Rijad Zuzo
2026-02-15 23:58:12 +01:00
parent c20c29550a
commit 85ca9fbeaf
7 changed files with 146 additions and 6 deletions
+1
View File
@@ -70,6 +70,7 @@ Behavior notes:
- android/iOS/macOS/windows: scheduled local notifications - android/iOS/macOS/windows: scheduled local notifications
- settings screen includes `Request Notification Access` action for runtime - settings screen includes `Request Notification Access` action for runtime
permission prompts permission prompts
- tapping a reminder notification opens reminder management UI in-app
## Local Persistence Backend ## Local Persistence Backend
+20 -1
View File
@@ -2,6 +2,25 @@
Updated: 2026-02-15 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 ## Latest Milestone (2026-02-15): Sync Rejection Repair Navigation
Added direct repair navigation from rejected sync items: Added direct repair navigation from rejected sync items:
@@ -242,7 +261,7 @@ Validation for this milestone:
1. Reminder delivery runtime integration: 1. Reminder delivery runtime integration:
- implemented for Android/iOS/macOS/windows with web/linux safe fallback. - implemented for Android/iOS/macOS/windows with web/linux safe fallback.
- permission UX implemented in settings. - permission UX implemented in settings.
- future enhancement: direct notification tap routing. - notification tap routing implemented for reminder screen handoff.
2. Conflict resolution UX: 2. Conflict resolution UX:
- reject/requeue + payload inspect + related-screen navigation implemented. - reject/requeue + payload inspect + related-screen navigation implemented.
- future enhancement: deep-link directly to specific entity edit form. - future enhancement: deep-link directly to specific entity edit form.
@@ -0,0 +1,18 @@
import 'dart:async';
/// Broadcasts reminder notification tap intents to the UI layer.
class ReminderNotificationIntentBus {
final StreamController<String> _controller =
StreamController<String>.broadcast();
Stream<String> get intents => _controller.stream;
void emitTap(String reminderId) {
if (reminderId.trim().isEmpty) {
return;
}
_controller.add(reminderId.trim());
}
Future<void> close() => _controller.close();
}
@@ -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<ReminderNotificationListener> createState() =>
_ReminderNotificationListenerState();
}
class _ReminderNotificationListenerState
extends ConsumerState<ReminderNotificationListener> {
StreamSubscription<String>? _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<void> _handleIntent(String reminderId) async {
if (!mounted) {
return;
}
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
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(),
);
}
}
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:relationship_saver/features/local/local_models.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:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
@@ -14,7 +15,10 @@ class LocalNotificationsReminderScheduler implements ReminderScheduler {
LocalNotificationsReminderScheduler({ LocalNotificationsReminderScheduler({
ReminderNotificationsDriver? driver, ReminderNotificationsDriver? driver,
DateTime Function()? now, DateTime Function()? now,
}) : _driver = driver ?? FlutterReminderNotificationsDriver(), ReminderNotificationIntentBus? intentBus,
}) : _driver =
driver ??
FlutterReminderNotificationsDriver(onTapPayload: intentBus?.emitTap),
_now = now ?? DateTime.now; _now = now ?? DateTime.now;
final ReminderNotificationsDriver _driver; final ReminderNotificationsDriver _driver;
@@ -122,10 +126,14 @@ abstract class ReminderNotificationsDriver {
class FlutterReminderNotificationsDriver class FlutterReminderNotificationsDriver
implements ReminderNotificationsDriver { implements ReminderNotificationsDriver {
FlutterReminderNotificationsDriver({FlutterLocalNotificationsPlugin? plugin}) FlutterReminderNotificationsDriver({
: _plugin = plugin ?? FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin? plugin,
void Function(String payload)? onTapPayload,
}) : _plugin = plugin ?? FlutterLocalNotificationsPlugin(),
_onTapPayload = onTapPayload;
final FlutterLocalNotificationsPlugin _plugin; final FlutterLocalNotificationsPlugin _plugin;
final void Function(String payload)? _onTapPayload;
Future<void>? _initializeFuture; Future<void>? _initializeFuture;
@override @override
@@ -204,6 +212,13 @@ class FlutterReminderNotificationsDriver
guid: 'ee23c0af-5898-4d96-b599-cf2d7d0d37d7', guid: 'ee23c0af-5898-4d96-b599-cf2d7d0d37d7',
), ),
), ),
onDidReceiveNotificationResponse: (NotificationResponse response) {
final String? payload = response.payload;
if (payload == null || payload.isEmpty) {
return;
}
_onTapPayload?.call(payload);
},
); );
} }
@@ -1,13 +1,30 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:relationship_saver/core/config/app_config.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.dart';
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_local_notifications.dart'; import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_local_notifications.dart';
/// Broadcasts reminder notification tap intents to interested UI listeners.
final Provider<ReminderNotificationIntentBus>
reminderNotificationIntentBusProvider = Provider<ReminderNotificationIntentBus>(
(Ref ref) {
final ReminderNotificationIntentBus bus = ReminderNotificationIntentBus();
ref.onDispose(() {
unawaited(bus.close());
});
return bus;
},
);
/// Provides reminder delivery scheduler implementation. /// Provides reminder delivery scheduler implementation.
final Provider<ReminderScheduler> reminderSchedulerProvider = final Provider<ReminderScheduler> reminderSchedulerProvider =
Provider<ReminderScheduler>((Ref ref) { Provider<ReminderScheduler>((Ref ref) {
if (AppConfig.enableLocalNotifications) { if (AppConfig.enableLocalNotifications) {
return LocalNotificationsReminderScheduler(); return LocalNotificationsReminderScheduler(
intentBus: ref.watch(reminderNotificationIntentBusProvider),
);
} }
return const NoopReminderScheduler(); return const NoopReminderScheduler();
}); });
+4 -1
View File
@@ -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/session_controller.dart';
import 'package:relationship_saver/features/auth/sign_in_view.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/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/features/sync/sync_background_runner.dart';
import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
@@ -28,7 +29,9 @@ class RelationshipSaverApp extends ConsumerWidget {
body: sessionState.when( body: sessionState.when(
data: (session) => session == null data: (session) => session == null
? const SignInView() ? const SignInView()
: const SyncBackgroundRunner(child: AppShell()), : const ReminderNotificationListener(
child: SyncBackgroundRunner(child: AppShell()),
),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (Object error, StackTrace stackTrace) => const SignInView(), error: (Object error, StackTrace stackTrace) => const SignInView(),
), ),