import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:timezone/data/latest.dart' as tz; import '../models/supplement.dart'; import 'database_helper.dart'; // Top-level function to handle notification responses when app is running @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { print('📱 === BACKGROUND NOTIFICATION RESPONSE ==='); print('📱 Action ID: ${notificationResponse.actionId}'); print('📱 Payload: ${notificationResponse.payload}'); print('📱 Notification ID: ${notificationResponse.id}'); print('📱 =========================================='); // For now, just log the action. The main app handler will process it. if (notificationResponse.actionId == 'take_supplement') { print('📱 BACKGROUND: Take action detected'); } else if (notificationResponse.actionId == 'snooze_10') { print('📱 BACKGROUND: Snooze action detected'); } } class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; NotificationService._internal(); final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); bool _isInitialized = false; static bool _engineInitialized = false; bool _permissionsRequested = false; // Callback for handling supplement intake from notifications Function(int supplementId, String supplementName, double units, String unitType)? _onTakeSupplementCallback; // Set callback for handling supplement intake from notifications void setTakeSupplementCallback(Function(int supplementId, String supplementName, double units, String unitType) callback) { _onTakeSupplementCallback = callback; } Future initialize() async { print('📱 Initializing NotificationService...'); if (_isInitialized) { print('📱 Already initialized'); return; } try { print('📱 Initializing timezones...'); print('📱 Engine initialized flag: $_engineInitialized'); if (!_engineInitialized) { tz.initializeTimeZones(); _engineInitialized = true; print('📱 Timezones initialized successfully'); } else { print('📱 Timezones already initialized, skipping'); } } catch (e) { print('📱 Warning: Timezone initialization issue (may already be initialized): $e'); _engineInitialized = true; // Mark as initialized to prevent retry } // Try to detect and set the local timezone more reliably try { // First try using the system timezone name final String timeZoneName = DateTime.now().timeZoneName; print('📱 System timezone name: $timeZoneName'); tz.Location? location; // Try common timezone mappings for your region if (timeZoneName.contains('CET') || timeZoneName.contains('CEST')) { location = tz.getLocation('Europe/Amsterdam'); // Netherlands } else if (timeZoneName.contains('UTC') || timeZoneName.contains('GMT')) { location = tz.getLocation('UTC'); } else { // Fallback: try to use the timezone name directly try { location = tz.getLocation(timeZoneName); } catch (e) { print('📱 Could not find timezone $timeZoneName, using Europe/Amsterdam as default'); location = tz.getLocation('Europe/Amsterdam'); } } tz.setLocalLocation(location); print('📱 Timezone set to: ${location.name}'); } catch (e) { print('📱 Error setting timezone: $e, using default'); // Fallback to a reasonable default for Netherlands tz.setLocalLocation(tz.getLocation('Europe/Amsterdam')); } print('📱 Current local time: ${tz.TZDateTime.now(tz.local)}'); print('📱 Current system time: ${DateTime.now()}'); const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); const DarwinInitializationSettings iosSettings = DarwinInitializationSettings( requestAlertPermission: false, // We'll request these separately requestBadgePermission: false, requestSoundPermission: false, ); const LinuxInitializationSettings linuxSettings = LinuxInitializationSettings( defaultActionName: 'Open notification', ); const InitializationSettings initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, linux: linuxSettings, ); print('📱 Initializing flutter_local_notifications...'); await _notifications.initialize( initSettings, onDidReceiveNotificationResponse: _onNotificationResponse, onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); // Test if notification response callback is working print('📱 Callback function is set and ready'); _isInitialized = true; print('📱 NotificationService initialization complete'); } // Handle notification responses (when user taps on notification or action) void _onNotificationResponse(NotificationResponse response) { print('📱 === NOTIFICATION RESPONSE ==='); print('📱 Action ID: ${response.actionId}'); print('📱 Payload: ${response.payload}'); print('📱 Notification ID: ${response.id}'); print('📱 Input: ${response.input}'); print('📱 ==============================='); if (response.actionId == 'take_supplement') { print('📱 Processing TAKE action...'); _handleTakeAction(response.payload, response.id); } else if (response.actionId == 'snooze_10') { print('📱 Processing SNOOZE action...'); _handleSnoozeAction(response.payload, 10, response.id); } else { print('📱 Default notification tap (no specific action)'); // Default tap (no actionId) opens the app normally } } void _handleTakeAction(String? payload, int? notificationId) { print('📱 === HANDLING TAKE ACTION ==='); print('📱 Payload received: $payload'); if (payload != null) { try { // Parse the payload to get supplement info final parts = payload.split('|'); print('📱 Payload parts: $parts (length: ${parts.length})'); if (parts.length >= 4) { final supplementId = int.parse(parts[0]); final supplementName = parts[1]; final units = double.parse(parts[2]); final unitType = parts[3]; print('📱 Parsed data:'); print('📱 - ID: $supplementId'); print('📱 - Name: $supplementName'); print('📱 - Units: $units'); print('📱 - Type: $unitType'); // Call the callback to record the intake if (_onTakeSupplementCallback != null) { print('📱 Calling supplement callback...'); _onTakeSupplementCallback!(supplementId, supplementName, units, unitType); print('📱 Callback completed'); } else { print('📱 ERROR: No callback registered!'); } // Mark notification as taken in database (this will cancel any pending retries) if (notificationId != null) { print('📱 Marking notification $notificationId as taken'); DatabaseHelper.instance.markNotificationTaken(notificationId); // Cancel any pending retry notifications for this notification _cancelRetryNotifications(notificationId); } // Show a confirmation notification print('📱 Showing confirmation notification...'); showInstantNotification( 'Supplement Taken!', '$supplementName has been recorded at ${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}', ); } else { print('📱 ERROR: Invalid payload format - not enough parts'); } } catch (e) { print('📱 ERROR in _handleTakeAction: $e'); } } else { print('📱 ERROR: Payload is null'); } print('📱 === TAKE ACTION COMPLETE ==='); } void _cancelRetryNotifications(int notificationId) { // Retry notifications use ID range starting from 200000 for (int i = 0; i < 10; i++) { // Cancel up to 10 potential retries int retryId = 200000 + (notificationId * 10) + i; _notifications.cancel(retryId); print('📱 Cancelled retry notification ID: $retryId'); } } void _handleSnoozeAction(String? payload, int minutes, int? notificationId) { print('📱 === HANDLING SNOOZE ACTION ==='); print('📱 Payload: $payload, Minutes: $minutes'); if (payload != null) { try { final parts = payload.split('|'); if (parts.length >= 2) { final supplementId = int.parse(parts[0]); final supplementName = parts[1]; print('📱 Snoozing supplement for $minutes minutes: $supplementName'); // Mark notification as snoozed in database (increment retry count) if (notificationId != null) { print('📱 Incrementing retry count for notification $notificationId'); DatabaseHelper.instance.incrementRetryCount(notificationId); } // Schedule a new notification for the snooze time final snoozeTime = tz.TZDateTime.now(tz.local).add(Duration(minutes: minutes)); print('📱 Snooze time: $snoozeTime'); _notifications.zonedSchedule( supplementId * 1000 + minutes, // Unique ID for snooze notifications 'Reminder: $supplementName', 'Snoozed reminder - Take your $supplementName now', snoozeTime, NotificationDetails( android: AndroidNotificationDetails( 'supplement_reminders', 'Supplement Reminders', channelDescription: 'Notifications for supplement intake reminders', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', ), ], ), iOS: const DarwinNotificationDetails(), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, payload: payload, ); showInstantNotification( 'Reminder Snoozed', '$supplementName reminder snoozed for $minutes minutes', ); print('📱 Snooze scheduled successfully'); } } catch (e) { print('📱 Error handling snooze action: $e'); } } print('📱 === SNOOZE ACTION COMPLETE ==='); } /// Check for persistent reminders from app context with settings Future checkPersistentReminders( bool persistentReminders, int reminderRetryInterval, int maxRetryAttempts, ) async { await schedulePersistentReminders( persistentReminders: persistentReminders, reminderRetryInterval: reminderRetryInterval, maxRetryAttempts: maxRetryAttempts, ); } /// Check for pending notifications that need retry and schedule them Future schedulePersistentReminders({ required bool persistentReminders, required int reminderRetryInterval, required int maxRetryAttempts, }) async { print('📱 Checking for pending notifications to retry...'); try { if (!persistentReminders) { print('📱 Persistent reminders disabled'); return; } print('📱 Retry settings: interval=$reminderRetryInterval min, max=$maxRetryAttempts attempts'); // Get all pending notifications from database final pendingNotifications = await DatabaseHelper.instance.getPendingNotifications(); print('📱 Found ${pendingNotifications.length} pending notifications'); final now = DateTime.now(); for (final notification in pendingNotifications) { final scheduledTime = DateTime.parse(notification['scheduledTime']); final retryCount = notification['retryCount'] as int; final lastRetryTime = notification['lastRetryTime'] != null ? DateTime.parse(notification['lastRetryTime']) : null; // Check if notification is overdue final timeSinceScheduled = now.difference(scheduledTime).inMinutes; final shouldRetry = timeSinceScheduled >= reminderRetryInterval; // Check if we haven't exceeded max retry attempts if (retryCount >= maxRetryAttempts) { print('📱 Notification ${notification['notificationId']} exceeded max attempts ($maxRetryAttempts)'); continue; } // Check if enough time has passed since last retry if (lastRetryTime != null) { final timeSinceLastRetry = now.difference(lastRetryTime).inMinutes; if (timeSinceLastRetry < reminderRetryInterval) { print('📱 Notification ${notification['notificationId']} not ready for retry yet'); continue; } } if (shouldRetry) { await _scheduleRetryNotification(notification, retryCount + 1); } } } catch (e) { print('📱 Error scheduling persistent reminders: $e'); } } Future _scheduleRetryNotification(Map notification, int retryAttempt) async { try { final notificationId = notification['notificationId'] as int; final supplementId = notification['supplementId'] as int; // Generate a unique ID for this retry (200000 + original_id * 10 + retry_attempt) final retryNotificationId = 200000 + (notificationId * 10) + retryAttempt; print('📱 Scheduling retry notification $retryNotificationId for supplement $supplementId (attempt $retryAttempt)'); // Get supplement details from database final supplements = await DatabaseHelper.instance.getAllSupplements(); final supplement = supplements.firstWhere((s) => s.id == supplementId && s.isActive, orElse: () => throw Exception('Supplement not found')); // Schedule the retry notification immediately await _notifications.show( retryNotificationId, 'Reminder: ${supplement.name}', 'Don\'t forget to take your ${supplement.name}! (Retry #$retryAttempt)', NotificationDetails( android: AndroidNotificationDetails( 'supplement_reminders', 'Supplement Reminders', channelDescription: 'Notifications for supplement intake reminders', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', showsUserInterface: true, icon: DrawableResourceAndroidBitmap('@drawable/ic_check'), ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', showsUserInterface: true, icon: DrawableResourceAndroidBitmap('@drawable/ic_snooze'), ), ], ), iOS: const DarwinNotificationDetails(), ), payload: '${supplement.id}|${supplement.name}|${supplement.numberOfUnits}|${supplement.unitType}|$notificationId', ); // Update the retry count in database await DatabaseHelper.instance.incrementRetryCount(notificationId); print('📱 Retry notification scheduled successfully'); } catch (e) { print('📱 Error scheduling retry notification: $e'); } } Future requestPermissions() async { print('📱 Requesting notification permissions...'); if (_permissionsRequested) { print('📱 Permissions already requested'); return true; } try { _permissionsRequested = true; final androidPlugin = _notifications.resolvePlatformSpecificImplementation(); if (androidPlugin != null) { print('📱 Requesting Android permissions...'); final granted = await androidPlugin.requestNotificationsPermission(); print('📱 Android permissions granted: $granted'); if (granted != true) { _permissionsRequested = false; return false; } } final iosPlugin = _notifications.resolvePlatformSpecificImplementation(); if (iosPlugin != null) { print('📱 Requesting iOS permissions...'); final granted = await iosPlugin.requestPermissions( alert: true, badge: true, sound: true, ); print('📱 iOS permissions granted: $granted'); if (granted != true) { _permissionsRequested = false; return false; } } print('📱 All permissions granted successfully'); return true; } catch (e) { _permissionsRequested = false; print('📱 Error requesting permissions: $e'); return false; } } Future scheduleSupplementReminders(Supplement supplement) async { print('📱 Scheduling reminders for ${supplement.name}'); print('📱 Reminder times: ${supplement.reminderTimes}'); // Cancel existing notifications for this supplement await cancelSupplementReminders(supplement.id!); for (int i = 0; i < supplement.reminderTimes.length; i++) { final timeStr = supplement.reminderTimes[i]; final timeParts = timeStr.split(':'); final hour = int.parse(timeParts[0]); final minute = int.parse(timeParts[1]); final notificationId = supplement.id! * 100 + i; // Unique ID for each reminder final scheduledTime = _nextInstanceOfTime(hour, minute); print('📱 Scheduling notification ID $notificationId for ${timeStr} -> ${scheduledTime}'); // Track this notification in the database await DatabaseHelper.instance.trackNotification( notificationId: notificationId, supplementId: supplement.id!, scheduledTime: scheduledTime.toLocal(), ); await _notifications.zonedSchedule( notificationId, 'Time for ${supplement.name}', 'Take ${supplement.numberOfUnits} ${supplement.unitType} (${supplement.ingredientsPerUnit})', scheduledTime, NotificationDetails( android: AndroidNotificationDetails( 'supplement_reminders', 'Supplement Reminders', channelDescription: 'Notifications for supplement intake reminders', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_save'), showsUserInterface: true, // Changed to true to open app ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_recent_history'), showsUserInterface: true, // Changed to true to open app ), ], ), iOS: const DarwinNotificationDetails(), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, matchDateTimeComponents: DateTimeComponents.time, payload: '${supplement.id}|${supplement.name}|${supplement.numberOfUnits}|${supplement.unitType}', ); print('📱 Successfully scheduled notification ID $notificationId'); } // Get all pending notifications to verify final pendingNotifications = await _notifications.pendingNotificationRequests(); print('📱 Total pending notifications: ${pendingNotifications.length}'); for (final notification in pendingNotifications) { print('📱 Pending: ID=${notification.id}, Title=${notification.title}'); } } Future cancelSupplementReminders(int supplementId) async { // Cancel all notifications for this supplement (up to 10 possible reminders) for (int i = 0; i < 10; i++) { final notificationId = supplementId * 100 + i; await _notifications.cancel(notificationId); } // Also clean up database tracking records for this supplement await DatabaseHelper.instance.clearNotificationTracking(supplementId); } Future cancelAllReminders() async { await _notifications.cancelAll(); } tz.TZDateTime _nextInstanceOfTime(int hour, int minute) { final tz.TZDateTime now = tz.TZDateTime.now(tz.local); tz.TZDateTime scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute); print('📱 Current time: $now (${now.timeZoneName})'); print('📱 Target time: ${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'); print('📱 Initial scheduled date: $scheduledDate (${scheduledDate.timeZoneName})'); if (scheduledDate.isBefore(now)) { scheduledDate = scheduledDate.add(const Duration(days: 1)); print('📱 Time has passed, scheduling for tomorrow: $scheduledDate (${scheduledDate.timeZoneName})'); } else { print('📱 Time is in the future, scheduling for today: $scheduledDate (${scheduledDate.timeZoneName})'); } return scheduledDate; } Future showInstantNotification(String title, String body) async { print('📱 Showing instant notification: $title - $body'); const NotificationDetails notificationDetails = NotificationDetails( android: AndroidNotificationDetails( 'instant_notifications', 'Instant Notifications', channelDescription: 'Instant notifications for supplement app', importance: Importance.high, priority: Priority.high, ), iOS: DarwinNotificationDetails(), ); await _notifications.show( DateTime.now().millisecondsSinceEpoch ~/ 1000, title, body, notificationDetails, ); print('📱 Instant notification sent'); } // Debug function to test notifications Future testNotification() async { print('📱 Testing notification system...'); await showInstantNotification('Test Notification', 'This is a test notification to verify the system is working.'); } // Debug function to schedule a test notification 1 minute from now Future testScheduledNotification() async { print('📱 Testing scheduled notification...'); final now = tz.TZDateTime.now(tz.local); final testTime = now.add(const Duration(minutes: 1)); print('📱 Scheduling test notification for: $testTime'); await _notifications.zonedSchedule( 99999, // Special ID for test notifications 'Test Scheduled Notification', 'This notification was scheduled 1 minute ago at ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}', testTime, const NotificationDetails( android: AndroidNotificationDetails( 'test_notifications', 'Test Notifications', channelDescription: 'Test notifications for debugging', importance: Importance.high, priority: Priority.high, ), iOS: DarwinNotificationDetails(), ), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, ); print('📱 Test notification scheduled successfully'); } // Debug function to get all pending notifications Future> getPendingNotifications() async { return await _notifications.pendingNotificationRequests(); } // Debug function to test notification actions Future testNotificationWithActions() async { print('📱 Creating test notification with actions...'); await _notifications.show( 88888, // Special test ID 'Test Action Notification', 'Tap Take or Snooze to test notification actions', NotificationDetails( android: AndroidNotificationDetails( 'test_notifications', 'Test Notifications', channelDescription: 'Test notifications for debugging actions', importance: Importance.high, priority: Priority.high, actions: [ AndroidNotificationAction( 'take_supplement', 'Take', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_save'), showsUserInterface: true, ), AndroidNotificationAction( 'snooze_10', 'Snooze 10min', icon: DrawableResourceAndroidBitmap('@android:drawable/ic_menu_recent_history'), showsUserInterface: true, ), ], ), iOS: const DarwinNotificationDetails(), ), payload: '999|Test Supplement|1.0|capsule', ); print('📱 Test notification with actions created'); } // Debug function to test basic notification tap response Future testBasicNotification() async { print('📱 Creating basic test notification...'); await _notifications.show( 77777, // Special test ID for basic notification 'Basic Test Notification', 'Tap this notification to test basic callback', NotificationDetails( android: AndroidNotificationDetails( 'test_notifications', 'Test Notifications', channelDescription: 'Test notifications for debugging', importance: Importance.high, priority: Priority.high, ), iOS: const DarwinNotificationDetails(), ), payload: 'basic_test', ); print('📱 Basic test notification created'); } }