MIF_E31230910_MP-HRIS-MOBILE/lib/services/websocket_service.dart

241 lines
6.5 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:web_socket_channel/web_socket_channel.dart';
import '../core/cache_manager.dart';
import '../core/constants/api_url.dart';
class WebSocketService extends ChangeNotifier {
static final WebSocketService _instance = WebSocketService._internal();
factory WebSocketService() => _instance;
WebSocketService._internal();
WebSocketChannel? _channel;
Timer? _reconnectTimer;
bool _isConnected = false;
bool _disposed = false;
String? _socketId;
final _eventController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get eventStream => _eventController.stream;
bool get isConnected => _isConnected;
Future<void> connect() async {
if (_isConnected || _disposed) return;
try {
final baseUrl = ApiUrl.baseUrl;
final token = CacheManager.authBox.get('auth_token') ?? '';
final wsUrl = _buildWsUrl(baseUrl);
if (wsUrl == null) return;
final uri = Uri.parse('$wsUrl/app/mpg-hris-key');
_channel = WebSocketChannel.connect(uri);
await _channel!.ready;
_isConnected = true;
_reconnectTimer?.cancel();
notifyListeners();
debugPrint('[WS] Terhubung ke $uri');
_channel!.stream.listen(
(message) => _handleMessage(message, token),
onDone: () {
debugPrint('[WS] Koneksi terputus');
_isConnected = false;
_socketId = null;
notifyListeners();
_scheduleReconnect();
},
onError: (error) {
debugPrint('[WS] Error: $error');
_isConnected = false;
_socketId = null;
notifyListeners();
_scheduleReconnect();
},
);
} catch (e) {
debugPrint('[WS] Gagal connect: $e');
_isConnected = false;
_scheduleReconnect();
}
}
String? _buildWsUrl(String apiBaseUrl) {
try {
final uri = Uri.parse(apiBaseUrl);
final scheme = uri.scheme == 'https' ? 'wss' : 'ws';
// Tunnel (ngrok / localtunnel): WebSocket langsung via tunnel
if (uri.host.contains('ngrok-free.dev') || uri.host.contains('loca.lt')) {
return 'wss://icy-bugs-hear.loca.lt';
}
if (uri.host.contains('trycloudflare.com')) {
return '$scheme://${uri.host}';
}
// Hosting: Reverb lewat Nginx proxy /app
if (uri.host.contains('hris-mp.web.id')) {
return 'wss://hris-mp.web.id';
}
// Lokal: Reverb langsung di port 8080
return '$scheme://${uri.host}:8080';
} catch (e) {
debugPrint('[WS] URL invalid: $e');
return null;
}
}
Future<void> _subscribeToChannels() async {
final userId = CacheManager.authBox.get('user_id');
if (userId == null) return;
await _sendSubscribe('private-user.$userId');
_sendSubscribePublic('pengumuman');
}
Future<void> _sendSubscribe(String channel) async {
if (_socketId == null) {
debugPrint('[WS] Tidak bisa subscribe $channel: socketId null');
return;
}
final token = CacheManager.authBox.get('auth_token') ?? '';
final authSignature = await _getChannelAuth(channel, token);
if (authSignature == null) {
debugPrint('[WS] Auth gagal untuk channel: $channel');
return;
}
final data = {
'event': 'pusher:subscribe',
'data': {
'channel': channel,
'auth': authSignature,
},
};
_channel?.sink.add(jsonEncode(data));
debugPrint('[WS] Subscribe (private): $channel');
}
void _sendSubscribePublic(String channel) {
final data = {
'event': 'pusher:subscribe',
'data': {
'channel': channel,
},
};
_channel?.sink.add(jsonEncode(data));
debugPrint('[WS] Subscribe (public): $channel');
}
Future<String?> _getChannelAuth(String channel, String token) async {
try {
final baseUrl = ApiUrl.baseUrl;
final authUrl = '$baseUrl/broadcasting/auth';
final response = await http.post(
Uri.parse(authUrl),
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
'socket_id': _socketId!,
'channel_name': channel,
},
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
final auth = json['auth'] as String?;
debugPrint('[WS] Auth berhasil untuk $channel');
return auth;
} else {
debugPrint('[WS] Auth error ${response.statusCode}: ${response.body}');
return null;
}
} catch (e) {
debugPrint('[WS] Auth exception: $e');
return null;
}
}
void _handleMessage(dynamic rawMessage, String token) {
try {
final message = jsonDecode(rawMessage as String) as Map<String, dynamic>;
final event = message['event'] as String?;
if (event == 'pusher:connection_established') {
final connData = jsonDecode(message['data'] as String);
_socketId = connData['socket_id'] as String?;
debugPrint('[WS] Connection established, socketId=$_socketId');
_subscribeToChannels();
return;
}
if (event == 'pusher_internal:subscription_succeeded') {
debugPrint('[WS] Subscription berhasil: ${message['channel']}');
return;
}
if (event == 'pusher:error') {
debugPrint('[WS] Pusher error: ${message['data']}');
return;
}
if (event != null && !event.startsWith('pusher')) {
final dataRaw = message['data'];
final data = dataRaw is String ? jsonDecode(dataRaw) : dataRaw;
_eventController.add({
'event': event,
'channel': message['channel'],
'data': data,
});
debugPrint('[WS] Event diterima: $event');
}
} catch (e) {
debugPrint('[WS] Parse error: $e');
}
}
void _scheduleReconnect() {
_reconnectTimer?.cancel();
if (_disposed) return;
_reconnectTimer = Timer(const Duration(seconds: 5), () {
debugPrint('[WS] Reconnecting...');
connect();
});
}
void disconnect() {
_reconnectTimer?.cancel();
_channel?.sink.close();
_channel = null;
_isConnected = false;
_socketId = null;
notifyListeners();
debugPrint('[WS] Disconnected');
}
@override
void dispose() {
_disposed = true;
disconnect();
_eventController.close();
super.dispose();
}
}