241 lines
6.5 KiB
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();
|
|
}
|
|
}
|