mirror of
https://github.com/vleeuwenmenno/supplements.git
synced 2025-09-11 18:29:12 +02:00
notification overhaul
This commit is contained in:
163
lib/services/notification_router.dart
Normal file
163
lib/services/notification_router.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:supplements/logging.dart';
|
||||
|
||||
import '../models/supplement.dart';
|
||||
import '../providers/supplement_provider.dart';
|
||||
import '../widgets/dialogs/bulk_take_dialog.dart';
|
||||
import '../widgets/dialogs/take_supplement_dialog.dart';
|
||||
|
||||
/// Centralizes routing from notification actions/taps to in-app UI.
|
||||
/// Handles both foreground/background taps and terminated-launch scenarios.
|
||||
class NotificationRouter {
|
||||
NotificationRouter._internal();
|
||||
static final NotificationRouter instance = NotificationRouter._internal();
|
||||
|
||||
GlobalKey<NavigatorState>? _navigatorKey;
|
||||
|
||||
void initialize(GlobalKey<NavigatorState> navigatorKey) {
|
||||
_navigatorKey = navigatorKey;
|
||||
}
|
||||
|
||||
Future<void> handleNotificationResponse(NotificationResponse response) async {
|
||||
final payloadMap = _decodePayload(response.payload);
|
||||
final actionId = response.actionId;
|
||||
printLog('🔔 handleNotificationResponse: actionId=$actionId payload=${response.payload} map=$payloadMap');
|
||||
|
||||
await _routeFromPayload(payloadMap);
|
||||
}
|
||||
|
||||
Future<void> handleAppLaunchDetails(NotificationAppLaunchDetails? details) async {
|
||||
if (details == null) return;
|
||||
if (!details.didNotificationLaunchApp) return;
|
||||
|
||||
final resp = details.notificationResponse;
|
||||
final payloadMap = _decodePayload(resp?.payload);
|
||||
printLog('🚀 App launched from notification: payload=${resp?.payload} map=$payloadMap');
|
||||
|
||||
await _routeFromPayload(payloadMap);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _decodePayload(String? payload) {
|
||||
if (payload == null || payload.isEmpty) return null;
|
||||
|
||||
// Try JSON first
|
||||
try {
|
||||
final map = jsonDecode(payload);
|
||||
if (map is Map<String, dynamic>) return map;
|
||||
} catch (_) {
|
||||
// Ignore and try fallback
|
||||
}
|
||||
|
||||
// Fallback: previous implementation used HH:mm as raw payload
|
||||
final hhmm = RegExp(r'^\d{2}:\d{2}$');
|
||||
if (hhmm.hasMatch(payload)) {
|
||||
return { 'type': 'group', 'time': payload };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _routeFromPayload(Map<String, dynamic>? payload) async {
|
||||
if (_navigatorKey == null) {
|
||||
printLog('⚠️ NotificationRouter not initialized with navigatorKey');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until navigator is ready and providers have loaded
|
||||
final ready = await _waitUntilReady(timeout: const Duration(seconds: 5));
|
||||
if (!ready) {
|
||||
printLog('⚠️ Timeout waiting for app to be ready for routing');
|
||||
return;
|
||||
}
|
||||
|
||||
final context = _navigatorKey!.currentContext!;
|
||||
final provider = context.read<SupplementProvider>();
|
||||
|
||||
if (payload == null) {
|
||||
printLog('⚠️ No payload to route');
|
||||
return;
|
||||
}
|
||||
|
||||
final type = payload['type'];
|
||||
if (type == 'single') {
|
||||
final id = payload['id'];
|
||||
if (id is int) {
|
||||
Supplement? s;
|
||||
try {
|
||||
s = provider.supplements.firstWhere((el) => el.id == id);
|
||||
} catch (_) {
|
||||
s = null;
|
||||
}
|
||||
if (s == null) {
|
||||
// Attempt reload once
|
||||
await provider.loadSupplements();
|
||||
try {
|
||||
s = provider.supplements.firstWhere((el) => el.id == id);
|
||||
} catch (_) {
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
if (s != null) {
|
||||
// For single: use the regular dialog (with time selection)
|
||||
// Ensure we close any existing dialog first
|
||||
_popAnyDialog(context);
|
||||
await showTakeSupplementDialog(context, s, hideTime: false);
|
||||
} else {
|
||||
printLog('⚠️ Supplement id=$id not found for single-take routing');
|
||||
_showSnack(context, 'Supplement not found');
|
||||
}
|
||||
}
|
||||
} else if (type == 'group') {
|
||||
final timeKey = payload['time'];
|
||||
if (timeKey is String) {
|
||||
// Build list of supplements scheduled at this timeKey
|
||||
final List<Supplement> list = provider.supplements.where((s) {
|
||||
return s.isActive && s.reminderTimes.contains(timeKey);
|
||||
}).toList();
|
||||
|
||||
if (list.isEmpty) {
|
||||
printLog('⚠️ No supplements found for group time=$timeKey');
|
||||
_showSnack(context, 'No supplements for $timeKey');
|
||||
return;
|
||||
}
|
||||
|
||||
_popAnyDialog(context);
|
||||
await showBulkTakeDialog(context, list);
|
||||
}
|
||||
} else {
|
||||
printLog('⚠️ Unknown payload type: $type');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _waitUntilReady({required Duration timeout}) async {
|
||||
final start = DateTime.now();
|
||||
while (DateTime.now().difference(start) < timeout) {
|
||||
final ctx = _navigatorKey!.currentContext;
|
||||
if (ctx != null) {
|
||||
final provider = Provider.of<SupplementProvider>(ctx, listen: false);
|
||||
if (!provider.isLoading) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _popAnyDialog(BuildContext context) {
|
||||
if (Navigator.of(context, rootNavigator: true).canPop()) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnack(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user