139 lines
4.2 KiB
Dart
139 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../models/notification_item.dart';
|
|
import '../providers/notifications_provider.dart';
|
|
import '../providers/notification_navigation_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;
|
|
});
|
|
|
|
// Listen for pending navigation from notification taps
|
|
ref.listen<PendingNavigation?>(pendingNotificationNavigationProvider, (
|
|
previous,
|
|
next,
|
|
) {
|
|
if (next != null) {
|
|
final type = next.type;
|
|
final id = next.id;
|
|
if (type == 'ticket') {
|
|
GoRouter.of(context).go('/tickets/$id');
|
|
} else if (type == 'task') {
|
|
GoRouter.of(context).go('/tasks/$id');
|
|
}
|
|
// Clear the pending navigation after handling
|
|
ref.read(pendingNotificationNavigationProvider.notifier).state = null;
|
|
}
|
|
});
|
|
|
|
return widget.child;
|
|
}
|
|
}
|