import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:solian/exceptions/request.dart'; import 'package:solian/models/notification.dart'; import 'package:solian/models/packet.dart'; import 'package:solian/models/pagination.dart'; import 'package:solian/platform.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/services.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class WebSocketProvider extends GetxController { RxBool isConnected = false.obs; RxBool isConnecting = false.obs; RxInt notificationUnread = 0.obs; RxList notifications = List.empty(growable: true).obs; WebSocketChannel? websocket; StreamController stream = StreamController.broadcast(); @override onInit() { notifyPrefetch(); super.onInit(); } void requestPermissions() { try { FirebaseMessaging.instance.requestPermission( alert: true, announcement: true, carPlay: true, badge: true, sound: true); } catch (_) { // When firebase isn't initialized (background service) FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.requestNotificationsPermission(); flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< IOSFlutterLocalNotificationsPlugin>() ?.requestPermissions( alert: true, badge: true, sound: true, ); flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< MacOSFlutterLocalNotificationsPlugin>() ?.requestPermissions( alert: true, badge: true, sound: true, ); } } Future connect({noRetry = false}) async { if (isConnected.value) { return; } else { disconnect(); } final AuthProvider auth = Get.find(); try { await auth.ensureCredentials(); final uri = Uri.parse(ServiceFinder.buildUrl( 'dealer', '/api/ws?tk=${auth.credentials!.accessToken}', ).replaceFirst('http', 'ws')); isConnecting.value = true; websocket = WebSocketChannel.connect(uri); await websocket?.ready; listen(); isConnected.value = true; } catch (err) { log('Unable connect dealer via websocket... $err'); if (!noRetry) { await auth.refreshCredentials(); return connect(noRetry: true); } } finally { isConnecting.value = false; } } void disconnect() { websocket?.sink.close(WebSocketStatus.normalClosure); websocket = null; isConnected.value = false; } void listen() { websocket?.stream.listen( (event) { final packet = NetworkPackage.fromJson(jsonDecode(event)); log('Websocket incoming message: ${packet.method} ${packet.message}'); stream.sink.add(packet); if (packet.method == 'notifications.new') { notifications.add(Notification.fromJson(packet.payload!)); notificationUnread.value++; } }, onDone: () { isConnected.value = false; Future.delayed(const Duration(seconds: 1), () => connect()); }, onError: (err) { isConnected.value = false; Future.delayed(const Duration(seconds: 3), () => connect()); }, ); } Future notifyPrefetch() async { final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse) return; final client = await auth.configureClient('auth'); final resp = await client.get('/notifications?skip=0&take=100'); if (resp.statusCode == 200) { final result = PaginationResult.fromJson(resp.body); final data = result.data?.map((x) => Notification.fromJson(x)).toList(); if (data != null) { notifications.addAll(data); notificationUnread.value = data.length; } } } Future registerPushNotifications() async { if (PlatformInfo.isWeb) return; final prefs = await SharedPreferences.getInstance(); if (prefs.getBool('service_background_notification') == true) { log('Background notification service has been enabled, skip register push notifications'); return; } final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse) return; late final String? token; late final String provider; var deviceUuid = await _getDeviceUuid(); if (deviceUuid == null || deviceUuid.isEmpty) { log("Unable to active push notifications, couldn't get device uuid"); return; } else { deviceUuid = md5.convert(utf8.encode(deviceUuid)).toString(); log('Device UUID is $deviceUuid'); } if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { provider = 'apple'; token = await FirebaseMessaging.instance.getAPNSToken(); } else { provider = 'firebase'; token = await FirebaseMessaging.instance.getToken(); } log('Device Push Token is $token'); final client = await auth.configureClient('auth'); final resp = await client.post('/notifications/subscribe', { 'provider': provider, 'device_token': token, 'device_id': deviceUuid, }); if (resp.statusCode != 200 && resp.statusCode != 400) { throw RequestException(resp); } } Future _getDeviceUuid() async { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (PlatformInfo.isWeb) { final webInfo = await deviceInfo.webBrowserInfo; return webInfo.vendor! + webInfo.userAgent! + webInfo.hardwareConcurrency.toString(); } if (PlatformInfo.isAndroid) { final androidInfo = await deviceInfo.androidInfo; return androidInfo.id; } if (PlatformInfo.isIOS) { final iosInfo = await deviceInfo.iosInfo; return iosInfo.identifierForVendor!; } if (PlatformInfo.isLinux) { final linuxInfo = await deviceInfo.linuxInfo; return linuxInfo.machineId!; } if (PlatformInfo.isWindows) { final windowsInfo = await deviceInfo.windowsInfo; return windowsInfo.deviceId; } if (PlatformInfo.isMacOS) { final macosInfo = await deviceInfo.macOsInfo; return macosInfo.systemGUID; } return null; } }