Route reminder notification taps into app UI
This commit is contained in:
@@ -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
@@ -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
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user