import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; /// WebSocket service for real-time chat communication class ChatWebSocketService { static final ChatWebSocketService _instance = ChatWebSocketService._internal(); factory ChatWebSocketService() => _instance; ChatWebSocketService._internal(); WebSocketChannel? _channel; StreamSubscription? _subscription; Timer? _pingTimer; Timer? _reconnectTimer; bool _isConnected = false; bool _isIntentionalClose = false; int _reconnectAttempts = 0; static const int _maxReconnectAttempts = 5; static const Duration _reconnectDelay = Duration(seconds: 3); // Callbacks Function(Map)? onMessageReceived; Function()? onConnected; Function()? onDisconnected; Function(dynamic error)? onError; String? _currentChatId; String? _wsUrl; /// Check if WebSocket is currently connected bool get isConnected => _isConnected; /// Get current chat ID String? get currentChatId => _currentChatId; /// Connect to WebSocket for a specific chat Future connect({ required String chatId, String baseUrl = "wss://taxglide.amrithaa.net:443/reverb", String appKey = "2yj0lyzc9ylw2h03ts6i", }) async { try { // Close existing connection if any await disconnect(); _currentChatId = chatId; _wsUrl = "$baseUrl/app/$appKey?protocol=7&client=js&version=1.0"; _isIntentionalClose = false; _reconnectAttempts = 0; await _establishConnection(); } catch (e) { debugPrint("❌ Connection error: $e"); onError?.call(e); _scheduleReconnect(); } } /// Establish WebSocket connection Future _establishConnection() async { try { debugPrint("🔌 Connecting to: $_wsUrl"); _channel = WebSocketChannel.connect(Uri.parse(_wsUrl!)); // Listen to messages _subscription = _channel!.stream.listen( _handleMessage, onError: _handleError, onDone: _handleDisconnection, cancelOnError: false, ); // Wait a bit for connection to establish await Future.delayed(const Duration(milliseconds: 500)); _isConnected = true; _reconnectAttempts = 0; debugPrint("✅ WebSocket connected"); // Subscribe to chat channel _subscribeToChannel(); // Start ping timer _startPingTimer(); onConnected?.call(); } catch (e) { debugPrint("❌ Failed to establish connection: $e"); _isConnected = false; rethrow; } } /// Handle incoming messages void _handleMessage(dynamic data) { try { debugPrint("📨 Received: $data"); final Map message = json.decode(data); // Handle Pusher ping if (message['event'] == 'pusher:ping') { _sendPong(); return; } // Handle connection established if (message['event'] == 'pusher:connection_established') { debugPrint("🔗 Connection established"); return; } // Handle subscription succeeded if (message['event'] == 'pusher_internal:subscription_succeeded') { debugPrint("✅ Subscribed to chat.${_currentChatId}"); return; } // Handle custom events (your chat messages) if (message['event'] != null && !message['event'].toString().startsWith('pusher')) { onMessageReceived?.call(message); } } catch (e) { debugPrint("❌ Error parsing message: $e"); onError?.call(e); } } /// Handle WebSocket errors void _handleError(dynamic error) { debugPrint("❌ WebSocket error: $error"); _isConnected = false; onError?.call(error); if (!_isIntentionalClose) { _scheduleReconnect(); } } /// Handle disconnection void _handleDisconnection() { debugPrint("🔌 WebSocket disconnected"); _isConnected = false; _stopPingTimer(); onDisconnected?.call(); if (!_isIntentionalClose) { _scheduleReconnect(); } } /// Subscribe to chat channel void _subscribeToChannel() { if (_currentChatId == null) return; final subscribeMessage = { 'event': 'pusher:subscribe', 'data': {'channel': 'chat.$_currentChatId'}, }; _sendMessage(subscribeMessage); debugPrint("📡 Subscribing to chat.$_currentChatId"); } /// Send pong response to ping void _sendPong() { final pongMessage = {'event': 'pusher:pong', 'data': {}}; _sendMessage(pongMessage); debugPrint("🏓 Pong sent"); } /// Start periodic ping timer void _startPingTimer() { _pingTimer?.cancel(); _pingTimer = Timer.periodic(const Duration(seconds: 30), (timer) { if (_isConnected) { // Server will send ping, we just respond with pong debugPrint("💓 Heartbeat check"); } }); } /// Stop ping timer void _stopPingTimer() { _pingTimer?.cancel(); _pingTimer = null; } /// Schedule reconnection attempt void _scheduleReconnect() { if (_isIntentionalClose) return; if (_reconnectAttempts >= _maxReconnectAttempts) { debugPrint("❌ Max reconnection attempts reached"); onError?.call( "Failed to reconnect after $_maxReconnectAttempts attempts", ); return; } _reconnectTimer?.cancel(); _reconnectAttempts++; debugPrint( "🔄 Scheduling reconnect attempt $_reconnectAttempts in ${_reconnectDelay.inSeconds}s", ); _reconnectTimer = Timer(_reconnectDelay, () async { if (!_isIntentionalClose && _wsUrl != null && _currentChatId != null) { debugPrint("🔄 Attempting to reconnect..."); try { await _establishConnection(); } catch (e) { debugPrint("❌ Reconnect failed: $e"); _scheduleReconnect(); } } }); } /// Send a message through WebSocket void _sendMessage(Map message) { if (_channel == null || !_isConnected) { debugPrint("⚠️ Cannot send message: Not connected"); return; } try { final jsonMessage = json.encode(message); _channel!.sink.add(jsonMessage); debugPrint("📤 Sent: $jsonMessage"); } catch (e) { debugPrint("❌ Error sending message: $e"); onError?.call(e); } } /// Send a custom event to the channel void sendEvent({ required String eventName, required Map data, }) { final message = { 'event': eventName, 'data': data, 'channel': 'chat.$_currentChatId', }; _sendMessage(message); } /// Disconnect from WebSocket Future disconnect() async { _isIntentionalClose = true; _reconnectTimer?.cancel(); _stopPingTimer(); await _subscription?.cancel(); await _channel?.sink.close(); _channel = null; _subscription = null; _isConnected = false; _currentChatId = null; debugPrint("🔌 WebSocket disconnected intentionally"); } /// Switch to a different chat channel Future switchChannel(String newChatId) async { if (newChatId == _currentChatId) return; debugPrint("🔄 Switching from chat.$_currentChatId to chat.$newChatId"); // Unsubscribe from current channel if (_currentChatId != null) { final unsubscribeMessage = { 'event': 'pusher:unsubscribe', 'data': {'channel': 'chat.$_currentChatId'}, }; _sendMessage(unsubscribeMessage); } // Update chat ID and subscribe to new channel _currentChatId = newChatId; _subscribeToChannel(); } /// Dispose and cleanup void dispose() { disconnect(); } }