tasq/lib/services/notification_bridge.dart

118 lines
3.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import '../models/notification_item.dart';
import '../providers/notifications_provider.dart';
/// Wraps the app and installs both a Supabase realtime listener and the
/// FCM handlers described in the frontend design.
///
/// The navigator key is required so that snackbars and navigation can be
/// triggered from outside the widget tree (e.g. from realtime callbacks).
class NotificationBridge extends ConsumerStatefulWidget {
const NotificationBridge({
required this.navigatorKey,
required this.child,
super.key,
});
final GlobalKey<NavigatorState> navigatorKey;
final Widget child;
@override
ConsumerState<NotificationBridge> createState() => _NotificationBridgeState();
}
class _NotificationBridgeState extends ConsumerState<NotificationBridge>
with WidgetsBindingObserver {
// store previous notifications to diff
List<NotificationItem> _prevList = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_setupFcmHandlers();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// App lifecycle is now monitored, but individual streams handle their own
// recovery via StreamRecoveryWrapper. This no longer forces a global reconnect,
// which was the blocking behavior users complained about.
if (state == AppLifecycleState.resumed) {
// Future: Could trigger stream-specific recovery hints if needed.
}
}
void _showBanner(String type, NotificationItem item) {
final ctx = widget.navigatorKey.currentState?.overlay?.context;
if (ctx == null) return;
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(
content: Text('New $type received!'),
action: SnackBarAction(
label: 'View',
onPressed: () => _navigateToNotification(item),
),
),
);
}
void _navigateToNotification(NotificationItem item) {
widget.navigatorKey.currentState?.pushNamed(
'/notification-detail',
arguments: item,
);
}
void _setupFcmHandlers() {
// ignore foreground messages; realtime websocket will surface them
FirebaseMessaging.onMessage.listen((_) {});
// handle taps when the app is backgrounded
FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageTap);
// handle a tap that launched the app from a terminated state
FirebaseMessaging.instance.getInitialMessage().then((msg) {
if (msg != null) _handleMessageTap(msg);
});
}
void _handleMessageTap(RemoteMessage message) {
final data = message.data.cast<String, dynamic>();
final item = NotificationItem.fromMap(data);
_navigateToNotification(item);
}
@override
Widget build(BuildContext context) {
// listen inside build; safe with ConsumerState
ref.listen<AsyncValue<List<NotificationItem>>>(notificationsProvider, (
previous,
next,
) {
final prevList = _prevList;
final nextList = next.maybeWhen(
data: (d) => d,
orElse: () => <NotificationItem>[],
);
if (nextList.length > prevList.length) {
final newItem = nextList.last;
_showBanner(newItem.type, newItem);
}
_prevList = nextList;
});
return widget.child;
}
}