import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/notifications/notification_permission_requester.dart'; import 'package:eatvio_app/core/push/hms_client_error.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/services.dart'; import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; class FirebaseApi { final ErrorReporter _errorReporter; final NotificationPermissionRequester _notificationPermissionRequester; final _firebaseMessaging = FirebaseMessaging.instance; FirebaseApi({ required ErrorReporter errorReporter, NotificationPermissionRequester? notificationPermissionRequester, }) : _errorReporter = errorReporter, _notificationPermissionRequester = notificationPermissionRequester ?? NotificationPermissionRequester(); Future getToken() async { final credentials = await _resolvePushCredentials(); return credentials.token; } Future _requestNotificationPermission() async { if (sl().buildType != AppBuildType.prod) return; try { await _notificationPermissionRequester.requestOnce( () => _firebaseMessaging.requestPermission(), ); } catch (e) { log('Failed to request Firebase messaging permission: $e'); } } Future registerAnonymousPushToken() async { try { await _requestNotificationPermission(); final deviceInfo = DeviceInfoPlugin(); final credentials = await _resolvePushCredentials(); final token = credentials.token; if (token == null || token.isEmpty) return; await sl().post( '/api/anonymous_token', data: { 'platform': credentials.platform, 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'token': token, }, ).timeout( const Duration(seconds: 10), onTimeout: () { throw TimeoutException('registerAnonymousPushToken timeout'); }, ); } catch (e, st) { log(e.toString(), stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi'); // Не пробрасываем ошибку дальше, так как это не критичная операция } } Future registerPushToken() async { if (await TokenStorage.haveTokenInStorage()) { try { await _requestNotificationPermission(); final deviceInfo = DeviceInfoPlugin(); final credentials = await _resolvePushCredentials(); final token = credentials.token; if (token == null || token.isEmpty) return; await sl().post( '/api/profile/notification/token/register', data: { 'platform': credentials.platform, 'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id, 'token': token, }, ).timeout( const Duration(seconds: 10), onTimeout: () { throw TimeoutException('registerPushToken timeout'); }, ); if (Platform.isIOS) { await _registerIosVoipPushToken(); } } catch (e, st) { log(e.toString(), stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi'); // Не пробрасываем ошибку дальше, так как это не критичная операция } } } Future unregisterPushTokens() async { if (!await TokenStorage.haveTokenInStorage()) return; try { await unregisterPushToken(); } catch (_) {} if (Platform.isIOS) { try { await _unregisterIosVoipPushToken(); } catch (_) {} } } Future unregisterPushToken() async { if (!await TokenStorage.haveTokenInStorage()) return; try { final credentials = await _resolvePushCredentials(); final token = credentials.token ?? ''; if (token.isEmpty) return; await sl().post( '/api/profile/notification/token/unregister', data: { 'platform': credentials.platform, 'token': token, }, ).timeout( const Duration(seconds: 10), onTimeout: () { throw TimeoutException('unregisterPushToken timeout'); }, ); } catch (e, st) { log(e.toString(), stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi'); } } Future _registerIosVoipPushToken() async { try { final token = (await FlutterCallkitIncoming.getDevicePushTokenVoIP())?.trim(); if (token == null || token.isEmpty) return; final deviceInfo = DeviceInfoPlugin(); await sl().post( '/api/profile/notification/token/voip/register', data: { 'platform': 'ios', 'hw_id': (await deviceInfo.iosInfo).identifierForVendor, 'token': token, }, ).timeout( const Duration(seconds: 10), onTimeout: () { throw TimeoutException('registerPushToken voip timeout'); }, ); } catch (e, st) { log(e.toString(), stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi'); } } Future _unregisterIosVoipPushToken() async { try { final token = (await FlutterCallkitIncoming.getDevicePushTokenVoIP())?.trim() ?? ''; await sl().post( '/api/profile/notification/token/voip/unregister', data: { 'token': token, }, ).timeout( const Duration(seconds: 10), onTimeout: () { throw TimeoutException('unregisterPushToken voip timeout'); }, ); } catch (e, st) { log(e.toString(), stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi'); } } Future<({String? token, String platform})> _resolvePushCredentials() async { if (Platform.isIOS) { return ( token: await _getFcmToken(), platform: pushPlatform(isIos: true, huaweiFamily: false, hmsAvailable: false, hasHmsToken: false), ); } final huaweiFamily = await _isHuaweiDevice(); if (huaweiFamily && await _isHmsAvailable()) { final token = await _getHuaweiPushTokenNative().timeout( const Duration(seconds: 5), onTimeout: () => null, ); if (token != null && token.isNotEmpty) { return ( token: token, platform: pushPlatform(isIos: false, huaweiFamily: true, hmsAvailable: true, hasHmsToken: true), ); } } return ( token: await _getFcmToken(), platform: pushPlatform(isIos: false, huaweiFamily: huaweiFamily, hmsAvailable: false, hasHmsToken: false), ); } Future _getFcmToken() { return _firebaseMessaging.getToken().timeout( const Duration(seconds: 5), onTimeout: () => null, ); } Future _isHuaweiDevice() async { if (!Platform.isAndroid) return false; try { final info = await DeviceInfoPlugin().androidInfo; return isHuaweiFamilyBrand(brand: info.brand, manufacturer: info.manufacturer); } catch (_) { return false; } } Future _isHmsAvailable() async { try { const channel = MethodChannel('com.eatvioapp/hms_push'); final available = await channel.invokeMethod('isHmsAvailable').timeout(const Duration(seconds: 5)); return available ?? false; } catch (e, st) { log('HMS availability check failed: $e', stackTrace: st); return false; } } Future _getHuaweiPushTokenNative() async { try { const channel = MethodChannel('com.eatvioapp/hms_push'); final String? token = await channel.invokeMethod('getHuaweiToken'); return token; } on PlatformException catch (e, st) { log(e.toString(), stackTrace: st); if (!isHmsClientUnavailable(e)) { await _errorReporter.report(e, st, origin: 'FirebaseApi'); } return null; } catch (e, st) { log(e.toString(), stackTrace: st); await _errorReporter.report(e, st, origin: 'FirebaseApi'); return null; } } }