import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import '../local/models/app_settings.dart'; import '../local/hive_boxes.dart'; import 'notification_inbox_service.dart'; import 'notification_service.dart'; import 'package:hive_flutter/hive_flutter.dart'; /// Phase-4 bridge for future FCM/APNs wiring. /// /// This app currently ships without Firebase/APNs SDK setup in source control. /// Once push SDK is configured, route incoming payloads to [ingestPayload]. class RemotePushService { RemotePushService._(); static final RemotePushService instance = RemotePushService._(); final NotificationInboxService _inbox = NotificationInboxService.instance; bool _initialized = false; Future init() async { if (_initialized) return; try { await Firebase.initializeApp(); final messaging = FirebaseMessaging.instance; await messaging.requestPermission( alert: true, badge: true, sound: true, ); FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); FirebaseMessaging.onMessage.listen((message) { unawaited(_handleMessage(message, isForeground: true)); }); FirebaseMessaging.onMessageOpenedApp.listen((message) { unawaited(_handleMessage(message, isForeground: false)); }); _initialized = true; } catch (_) { // Firebase may not be configured in all build variants yet. } } Future ingestPayload( Map payload, { AppSettings? settings, }) async { if (settings != null && !settings.inboxEnabled) return; final id = (payload['id'] ?? payload['messageId'] ?? '').toString().trim(); final title = (payload['title'] ?? '').toString().trim(); final body = (payload['body'] ?? '').toString().trim(); if (id.isEmpty || title.isEmpty || body.isEmpty) return; final type = (payload['type'] ?? 'content').toString().trim(); final deeplink = (payload['deeplink'] ?? '').toString().trim(); final expiresAt = DateTime.tryParse((payload['expiresAt'] ?? '').toString().trim()); final isPinned = payload['isPinned'] == true; await _inbox.addItem( title: title, body: body, type: type.isEmpty ? 'content' : type, source: 'remote', deeplink: deeplink.isEmpty ? null : deeplink, dedupeKey: 'remote.push.$id', expiresAt: expiresAt, isPinned: isPinned, meta: {'remoteId': id}, ); } Future _handleMessage( RemoteMessage message, { required bool isForeground, }) async { final payload = { 'id': message.messageId ?? message.data['id'] ?? '', 'title': message.notification?.title ?? message.data['title'] ?? '', 'body': message.notification?.body ?? message.data['body'] ?? '', 'type': message.data['type'] ?? 'content', 'deeplink': message.data['deeplink'] ?? '', 'expiresAt': message.data['expiresAt'] ?? '', 'isPinned': message.data['isPinned'] == 'true', }; final settings = Hive.box(HiveBoxes.settings).get('default') ?? AppSettings(); await ingestPayload(payload, settings: settings); if (isForeground && settings.alertsEnabled && (payload['title'] as String).trim().isNotEmpty && (payload['body'] as String).trim().isNotEmpty) { await NotificationService.instance.showNonPrayerAlert( settings: settings, id: NotificationService.instance .nonPrayerNotificationId('remote.${payload['id']}'), title: (payload['title'] as String).trim(), body: (payload['body'] as String).trim(), payloadType: 'remote', bypassDailyCap: true, ); } } } @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { WidgetsFlutterBinding.ensureInitialized(); await initHive(); try { await Firebase.initializeApp(); } catch (_) {} final payload = { 'id': message.messageId ?? message.data['id'] ?? '', 'title': message.notification?.title ?? message.data['title'] ?? '', 'body': message.notification?.body ?? message.data['body'] ?? '', 'type': message.data['type'] ?? 'content', 'deeplink': message.data['deeplink'] ?? '', 'expiresAt': message.data['expiresAt'] ?? '', 'isPinned': message.data['isPinned'] == 'true', }; final settings = Hive.box(HiveBoxes.settings).get('default') ?? AppSettings(); await RemotePushService.instance.ingestPayload(payload, settings: settings); }