import 'dart:async'; import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:taxglide/services/notification_service.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:taxglide/controller/api_contoller.dart'; class NotificationWebSocket { static final NotificationWebSocket _instance = NotificationWebSocket._internal(); factory NotificationWebSocket() => _instance; NotificationWebSocket._internal(); WebSocketChannel? _channel; StreamSubscription? _subscription; Timer? _reconnectTimer; bool _isConnected = false; bool _isIntentionalClose = false; String? _userId; String? _wsUrl; ProviderContainer? _container; int _reconnectAttempts = 0; static const int _maxReconnectAttempts = 10; /// ChatId the user is currently VIEWING. String? _activeChatId; /// ChatId whose `chat.` channel we are subscribed to on this WS. String? _subscribedChatChannelId; // ─────────────────────────── Public API ──────────────────────────────────── /// Call from LiveChatScreen.initState(). void setActiveChatId(String chatId) { _activeChatId = chatId; debugPrint('NotificationWS: viewing chat $chatId'); _subscribeToChatChannel(chatId); } /// Call from LiveChatScreen.dispose() void clearActiveChatId() { debugPrint('NotificationWS: left chat $_activeChatId'); _activeChatId = null; } /// Subscribe to a chat channel for updates. void watchChatChannel(String chatId) { debugPrint('Eye NotificationWS: watchChatChannel($chatId)'); _subscribeToChatChannel(chatId); } /// Connect (or skip if already connected for the same user). Future connect({ required String userId, required ProviderContainer container, String baseUrl = 'wss://taxglide.amrithaa.net:443/reverb', String appKey = '2yj0lyzc9ylw2h03ts6i', }) async { if (_isConnected && _userId == userId) { debugPrint('⚡ NotificationWS: already connected for user $userId'); return; } await disconnect(); _userId = userId; _container = container; _wsUrl = '$baseUrl/app/$appKey?protocol=7&client=js&version=1.0'; _isIntentionalClose = false; _reconnectAttempts = 0; _openSocket(); } // ──────────────────────── Socket lifecycle ────────────────────────────────── void _openSocket() { try { debugPrint('🔌 NotificationWS connecting…'); _channel = WebSocketChannel.connect(Uri.parse(_wsUrl!)); _subscription = _channel!.stream.listen( _handleMessage, onError: _handleError, onDone: _handleDone, ); _isConnected = true; _subscribeToUserChannel(); // Re-subscribe to chat channel after reconnect if we had one before. final chatId = _subscribedChatChannelId; if (chatId != null) { _sendRaw({ 'event': 'pusher:subscribe', 'data': {'channel': 'chat.$chatId'}, }); debugPrint('📡 NotificationWS: re-subscribed to chat.$chatId'); } } catch (e) { debugPrint('❌ NotificationWS connect error: $e'); _isConnected = false; _scheduleReconnect(); } } void _subscribeToUserChannel() { if (_userId == null) return; _sendRaw({ 'event': 'pusher:subscribe', 'data': {'channel': 'user-chat-notification.$_userId'}, }); debugPrint( '📡 NotificationWS: subscribed to user-chat-notification.$_userId', ); } void _subscribeToChatChannel(String chatId) { if (_subscribedChatChannelId == chatId) return; // already subscribed // Unsubscribe previous chat channel if (_subscribedChatChannelId != null) { _sendRaw({ 'event': 'pusher:unsubscribe', 'data': {'channel': 'chat.$_subscribedChatChannelId'}, }); debugPrint( '📡 NotificationWS: unsubscribed from chat.$_subscribedChatChannelId', ); } _subscribedChatChannelId = chatId; _sendRaw({ 'event': 'pusher:subscribe', 'data': {'channel': 'chat.$chatId'}, }); debugPrint('📡 NotificationWS: subscribed to chat.$chatId'); } // ──────────────────────── Message handler ────────────────────────────────── void _handleMessage(dynamic raw) { debugPrint('🔔 NotificationWS raw: $raw'); try { final msg = jsonDecode(raw) as Map; final event = msg['event']?.toString() ?? ''; // ── Pusher connection established → re-subscribe ────────────────────── if (event == 'pusher:connection_established') { debugPrint('🔗 NotificationWS: connection established'); _subscribeToUserChannel(); final chatId = _subscribedChatChannelId; if (chatId != null) { _sendRaw({ 'event': 'pusher:subscribe', 'data': {'channel': 'chat.$chatId'}, }); } return; } // ── Skip all other Pusher internal events ───────────────────────────── if (event.startsWith('pusher')) return; if (event.startsWith('pusher_internal')) return; // ── Refresh Riverpod badge count ────────────────────────────────────── try { _container?.read(notificationTriggerProvider.notifier).state++; } catch (_) {} // ── Determine which chat this message belongs to ────────────────────── String? incomingChatId; final channelStr = msg['channel']?.toString() ?? ''; if (channelStr.startsWith('chat.')) { incomingChatId = channelStr.split('.').last; } // ── Parse the payload ───────────────────────────────────────────────── final rawData = msg['data']; if (rawData == null) return; dynamic data; try { data = rawData is String ? jsonDecode(rawData) : rawData; } catch (_) { data = rawData; } // Flatten list → use first element if (data is List && data.isNotEmpty) { data = data[0]; } if (data is! Map) { debugPrint('⚠️ NotificationWS: unrecognised data format, skipping'); return; } final dataMap = Map.from(data); debugPrint('🔔 NotificationWS data keys: ${dataMap.keys.toList()}'); // ── Skip ping-only payloads {status, reload} ────────────────────────── // The backend first fires a lightweight notification-trigger event that // only contains `status` and `reload`. There is nothing to display yet; // the full message list arrives in the next event on the same channel. final onlyStatusReload = dataMap.containsKey('status') && dataMap.containsKey('reload') && !dataMap.containsKey('message') && !dataMap.containsKey('chat_by'); if (onlyStatusReload) { debugPrint('🔔 NotificationWS: reload ping received, badge refreshed'); return; } // ── Skip user's own messages ────────────────────────────────────────── final chatBy = dataMap['chat_by']?.toString().toLowerCase() ?? ''; if (chatBy == 'user' || chatBy == 'customer') { debugPrint('🔕 NotificationWS: own message, skipping'); return; } // ── Extract routing info if needed for internal state ───────────── final String? page = dataMap['page']?.toString() ?? dataMap['type']?.toString(); final String? pageId = dataMap['page_id']?.toString() ?? dataMap['pageId']?.toString(); if (page == null || page.isEmpty || pageId == null || pageId.isEmpty) { // Chat Notification fallback incomingChatId ??= dataMap['chat_id']?.toString() ?? dataMap['chatId']?.toString() ?? (dataMap['parent_tag'] != null && dataMap['parent_tag'] is Map ? dataMap['parent_tag']['id']?.toString() : null) ?? dataMap['id']?.toString(); if (incomingChatId != null && incomingChatId != '0') { _subscribeToChatChannel(incomingChatId); } } // ── Show Foreground Notification ───────────────────────────────────── // Only show if it's NOT a message for the currently active chat screen. // Pass incomingChatId as the dedup tag so that if FCM already fired a // notification for this same chat, this one *replaces* it on Android // instead of showing a second notification. if (incomingChatId != _activeChatId) { // Prefer the sender name from dataMap for the title. final String senderTitle = dataMap['sender_name']?.toString() ?? dataMap['name']?.toString() ?? dataMap['chat_by']?.toString() ?? 'New Message'; final RemoteMessage fcmLikeMessage = RemoteMessage( data: dataMap, notification: RemoteNotification( title: senderTitle, body: dataMap['message']?.toString() ?? 'You have a new message', ), ); Get.find().showForegroundNotification( fcmLikeMessage, tag: incomingChatId, // dedup key – same chat collapses into 1 notif ); } } catch (e, st) { debugPrint('❌ NotificationWS _handleMessage error: $e\n$st'); } } void _sendRaw(Map message) { try { _channel?.sink.add(jsonEncode(message)); } catch (e) { debugPrint('❌ NotificationWS send error: $e'); } } // ──────────────────────── Reconnect ──────────────────────────────────────── void _handleError(dynamic error) { debugPrint('❌ NotificationWS error: $error'); _isConnected = false; _scheduleReconnect(); } void _handleDone() { debugPrint('🔌 NotificationWS: connection closed'); _isConnected = false; if (!_isIntentionalClose) _scheduleReconnect(); } void _scheduleReconnect() { if (_isIntentionalClose || _reconnectAttempts >= _maxReconnectAttempts) { return; } _reconnectAttempts++; final delay = Duration(seconds: _reconnectAttempts * 2); debugPrint( '🔄 NotificationWS: reconnect #$_reconnectAttempts in ${delay.inSeconds}s', ); _reconnectTimer = Timer(delay, () { if (!_isIntentionalClose) _openSocket(); }); } Future disconnect() async { _isIntentionalClose = true; _reconnectTimer?.cancel(); await _subscription?.cancel(); await _channel?.sink.close(); _isConnected = false; _subscribedChatChannelId = null; debugPrint('🔌 NotificationWS: disconnected'); } }