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? _navigatorKey; void initialize(GlobalKey navigatorKey) { _navigatorKey = navigatorKey; } Future 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 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? _decodePayload(String? payload) { if (payload == null || payload.isEmpty) return null; // Try JSON first try { final map = jsonDecode(payload); if (map is Map) 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 _routeFromPayload(Map? 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(); 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 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 _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(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)), ); } }