diff --git a/lib/core/di/service_locator.dart b/lib/core/di/service_locator.dart new file mode 100644 index 00000000..34a10706 --- /dev/null +++ b/lib/core/di/service_locator.dart @@ -0,0 +1,19 @@ +import 'package:get_it/get_it.dart'; +import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart'; +/// Global service locator instance +final getIt = GetIt.instance; +/// Setup dependency injection +Future setupServiceLocator() async { + // SignalR Service - ONLY ONE instance for the entire app + getIt.registerLazySingleton( + () => SignalRService(), + ); + print('✅ [DI] Service locator initialized'); + print(' - SignalRService registered as singleton (hashCode: ${getIt().hashCode})'); +} + +Future resetServices() async { + await getIt().reset(); + print('✅ [DI] All services reset'); +} + diff --git a/lib/main.dart b/lib/main.dart index 40190c8f..30719d7d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,6 +36,7 @@ import 'package:test_sa/modules/asset_inventory_module/provider/asset_inventory_ import 'package:test_sa/modules/cm_module/cm_detail_provider.dart'; import 'package:test_sa/modules/cm_module/create_cm_request.dart'; import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; +import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart'; import 'package:test_sa/modules/demo_module/create_demo_request_page.dart'; import 'package:test_sa/modules/demo_module/provider/demo_period_lookup_provider.dart'; import 'package:test_sa/modules/demo_module/provider/demo_provider.dart'; @@ -145,6 +146,7 @@ import 'providers/service_request_providers/loan_availability_provider.dart'; import 'providers/service_request_providers/reject_reason_provider.dart'; import 'modules/cleaner_module/pages/cleaner_search_form_view.dart'; import 'modules/cleaner_module/provider/cleaner_provider.dart'; +import 'package:test_sa/core/di/service_locator.dart'; // class MyHttpOverrides extends HttpOverrides { // @override @@ -159,7 +161,7 @@ void main() async { // HttpOverrides.global = MyHttpOverrides(); // for later use. _configureLocalTimeZone(); NotificationManger.initialisation((notificationDetails) {}, (id, title, body, payload) async {}); - + await setupServiceLocator(); if (Platform.isIOS) { await Firebase.initializeApp( options: const FirebaseOptions( @@ -173,7 +175,6 @@ void main() async { await Firebase.initializeApp(); } - SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.white, diff --git a/lib/modules/cx_module/chat/call/audio_call_page.dart b/lib/modules/cx_module/chat/call/audio_call_page.dart index bc3cf70e..30bf50eb 100644 --- a/lib/modules/cx_module/chat/call/audio_call_page.dart +++ b/lib/modules/cx_module/chat/call/audio_call_page.dart @@ -6,7 +6,7 @@ import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; -import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; +import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; import 'package:test_sa/modules/cx_module/chat/model/call_session.dart'; import 'package:test_sa/modules/cx_module/chat/call/video_call_page.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; @@ -21,35 +21,26 @@ class AudioCallPage extends StatefulWidget { } class _AudioCallPageState extends State { + late CallManager _callManager; + @override void initState() { super.initState(); + _callManager = CallManager(); // Listen for call end and navigate back - WidgetsBinding.instance.addPostFrameCallback((_) { - final chatProvider = context.read(); - // Add listener to detect when call ends - chatProvider.addListener(_checkCallStatus); - }); + _callManager.addListener(_checkCallStatus); } void _checkCallStatus() { - final chatProvider = context.read(); - // If call status is idle (call ended), navigate back to chat - if (chatProvider.callStatus == CallStatus.idle && mounted) { - // Remove listener before popping to avoid memory leaks - chatProvider.removeListener(_checkCallStatus); + // If call status is idle (call ended), navigate back + if (_callManager.callStatus == CallStatus.idle && mounted) { Navigator.of(context).pop(); } } @override void dispose() { - // Clean up listener - try { - context.read().removeListener(_checkCallStatus); - } catch (e) { - // Provider might already be disposed - } + _callManager.removeListener(_checkCallStatus); super.dispose(); } @@ -62,9 +53,10 @@ class _AudioCallPageState extends State { arrowBackColor: AppColor.white10, ), body: SafeArea( - child: Selector( - selector: (_, provider) => provider.currentCall, - builder: (context, session, _) { + child: ListenableBuilder( + listenable: _callManager, + builder: (context, _) { + final session = _callManager.currentCall; if (session == null) { return const Center(child: Text('No active call')); } @@ -110,6 +102,8 @@ class _AudioCallPageState extends State { } Widget _buildContactInfo(BuildContext context, CallSession session) { + final isPeerMuted = _callManager.isPeerMuted; + return Column( children: [ Stack( @@ -120,7 +114,6 @@ class _AudioCallPageState extends State { decoration: BoxDecoration( shape: BoxShape.circle, color: AppColor.whiteF8d, - //TODO need to check opacity border: Border.all(color: AppColor.white10.withOpacity(0.2), width: 1), ), child: ClipOval( @@ -139,30 +132,24 @@ class _AudioCallPageState extends State { ), ), // Show mute indicator when peer is muted - Selector( - selector: (_, provider) => provider.isPeerMuted, - builder: (context, isPeerMuted, _) { - if (!isPeerMuted) return const SizedBox.shrink(); - - return Positioned( - bottom: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColor.red30, - shape: BoxShape.circle, - border: Border.all(color: AppColor.white10, width: 2), - ), - child: 'mic_disable'.toSvgAsset( - width: 16, - height: 16, - color: AppColor.white10, - ), + if (isPeerMuted) + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColor.red30, + shape: BoxShape.circle, + border: Border.all(color: AppColor.white10, width: 2), ), - ); - }, - ), + child: 'mic_disable'.toSvgAsset( + width: 16, + height: 16, + color: AppColor.white10, + ), + ), + ), ], ), 24.height, @@ -175,174 +162,131 @@ class _AudioCallPageState extends State { textAlign: TextAlign.center, ), // Show "Microphone is off" text when peer is muted - Selector( - selector: (_, provider) => provider.isPeerMuted, - builder: (context, isPeerMuted, _) { - if (!isPeerMuted) return const SizedBox.shrink(); - - return Column( - children: [ - 8.height, - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: AppColor.red30.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - 'mic_disable'.toSvgAsset( - width: 14, - height: 14, + if (isPeerMuted) + Column( + children: [ + 8.height, + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColor.red30.withOpacity(0.3), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + 'mic_disable'.toSvgAsset( + width: 14, + height: 14, + color: AppColor.white10, + ), + 6.width, + Text( + 'Microphone is off', + style: AppTextStyles.bodyText2.copyWith( color: AppColor.white10, + fontSize: 12, ), - 6.width, - Text( - 'Microphone is off', - style: AppTextStyles.bodyText2.copyWith( - color: AppColor.white10, - fontSize: 12, - ), - ), - ], - ), + ), + ], ), - ], - ); - }, - ), + ), + ], + ), ], ); } Widget _buildCallStatusOrDuration(BuildContext context) { - return Selector( - selector: (_, provider) => provider.callStatus, - builder: (context, status, _) { - String statusText; - switch (status) { - case CallStatus.outgoingRinging: - statusText = 'Calling...'; - break; - case CallStatus.incomingRinging: - statusText = 'Incoming call...'; - break; - case CallStatus.connecting: - statusText = 'Connecting...'; - break; - case CallStatus.connected: - return _buildDuration(context); - default: - statusText = ''; - } - return Text( - statusText, - style: AppTextStyles.heading5.copyWith( - color: AppColor.neutral100, - ), - ); - }, + final status = _callManager.callStatus; + final duration = _callManager.callDuration; + + String statusText; + if (status == CallStatus.connected) { + // Show call duration + final minutes = duration.inMinutes; + final seconds = duration.inSeconds % 60; + statusText = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } else { + // Show call status + statusText = _getStatusText(status); + } + + return Text( + statusText, + style: AppTextStyles.bodyText.copyWith(color: Colors.white70), ); } - Widget _buildDuration(BuildContext context) { - return Selector( - selector: (_, provider) => provider.callDuration, - builder: (context, duration, _) { - if (duration.inSeconds == 0) { - return const SizedBox.shrink(); - } + String _getStatusText(CallStatus status) { + switch (status) { + case CallStatus.connecting: + return 'Connecting...'; + case CallStatus.incomingRinging: + return 'Incoming call...'; + case CallStatus.outgoingRinging: + return 'Ringing...'; + case CallStatus.connected: + return 'Connected'; + default: + return ''; + } + } - String twoDigits(int n) => n.toString().padLeft(2, '0'); - final minutes = twoDigits(duration.inMinutes.remainder(60)); - final seconds = twoDigits(duration.inSeconds.remainder(60)); + Widget _buildControls(BuildContext context, CallSession session) { + final isMuted = _callManager.isMuted; + final isSpeakerOn = _callManager.isSpeakerOn; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.white.withAlpha(51), - borderRadius: BorderRadius.circular(20), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildControlButton( + icon: isMuted ? Icons.mic_off : Icons.mic, + label: isMuted ? 'Unmute' : 'Mute', + onPressed: () => _callManager.toggleMute(), + backgroundColor: isMuted ? Colors.red : Colors.white24, ), - child: Text( - '$minutes:$seconds', - style: AppTextStyles.heading5.copyWith( - color: AppColor.neutral100, - fontWeight: FontWeight.w400, - ), + _buildControlButton( + icon: isSpeakerOn ? Icons.volume_up : Icons.volume_down, + label: isSpeakerOn ? 'Speaker' : 'Earpiece', + onPressed: () => _callManager.toggleSpeaker(), + backgroundColor: isSpeakerOn ? Colors.blue : Colors.white24, ), - ); - }, - ); - } - - Widget _buildControls(BuildContext context, CallSession session) { - final chatProvider = context.read(); - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Selector( - selector: (_, provider) => provider.isMuted, - builder: (context, isMuted, _) { - return _buildControlButton( - icon: isMuted ? 'mic_disable' : 'mic_enable', - label: isMuted ? 'Unmute' : 'Mute', - isActive: isMuted, - onPressed: () { - chatProvider.toggleMute(); - }, - ); - }, - ), - // _buildControlButton( - // icon: 'video_call_icon', - // label: 'Video', - // isActive: false, - // onPressed: () { - // // Switch to video call view - // Navigator.pushReplacement( - // context, - // MaterialPageRoute( - // builder: (context) => const VideoCallPage(), - // ), - // ); - // }, - // ), - Selector( - selector: (_, provider) => provider.isSpeakerOn, - builder: (context, isSpeakerOn, _) { - return _buildControlButton( - icon: 'speaker_enable', - label: 'Speaker', - isActive: isSpeakerOn, - onPressed: () { - chatProvider.toggleSpeaker(); - }, - ); - }, - ), - ], - ), - 48.height, - _buildEndCallButton(context, chatProvider), - ], + _buildControlButton( + icon: Icons.call_end, + label: 'End', + onPressed: () => _callManager.hangUp(), + backgroundColor: Colors.red, + iconColor: Colors.white, + ), + ], + ), ); } Widget _buildControlButton({ - required String icon, + required IconData icon, required String label, - required bool isActive, required VoidCallback onPressed, + Color? backgroundColor, + Color? iconColor, }) { return Column( children: [ Container( padding: const EdgeInsets.all(18), - decoration: BoxDecoration(color: isActive ? AppColor.primary10 : AppColor.black2E, shape: BoxShape.circle, border: BoxBorder.all(color: AppColor.white10.withOpacity(0.2))), - child: icon.toSvgAsset(width: 24, height: 24, color: AppColor.white10), + decoration: BoxDecoration( + color: backgroundColor ?? AppColor.black2E, + shape: BoxShape.circle, + border: Border.all(color: AppColor.white10.withOpacity(0.2)), + ), + child: Icon( + icon, + size: 24, + color: iconColor ?? AppColor.white10, + ), ).onPress(() { onPressed(); }), @@ -354,26 +298,4 @@ class _AudioCallPageState extends State { ], ); } - - Widget _buildEndCallButton(BuildContext context, ChatProvider provider) { - return Container( - padding: const EdgeInsets.all(18), - decoration: BoxDecoration( - color: AppColor.red30, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - alignment: Alignment.center, - child: 'end_call'.toSvgAsset(height: 32, width: 32, color: AppColor.white10), - ).onPress(() async { - await provider.hangUp(); - // Removed Navigator.pop() - automatic listener will handle navigation - }); - } -} \ No newline at end of file +} diff --git a/lib/modules/cx_module/chat/call/services/call_notification_service.dart b/lib/modules/cx_module/chat/call/services/call_notification_service.dart index de4dfa98..c825838d 100644 --- a/lib/modules/cx_module/chat/call/services/call_notification_service.dart +++ b/lib/modules/cx_module/chat/call/services/call_notification_service.dart @@ -3,21 +3,12 @@ import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; -/// Service to handle incoming call notifications across all app states -/// Integrates with existing Firebase notification system class CallNotificationService { static final CallNotificationService _instance = CallNotificationService._internal(); factory CallNotificationService() => _instance; CallNotificationService._internal(); - - // Track processed call IDs to prevent duplicates final Set _processedCallIds = {}; - - // Store pending call data for restoration after app wake Map? _pendingCallData; - - /// Handle incoming call notification from push (FCM/Huawei) or SignalR - /// This is the SINGLE entry point for all incoming calls regardless of app state Future handleIncomingCallNotification({ required Map notificationData, required String source, // 'push' or 'signalr' @@ -26,12 +17,9 @@ class CallNotificationService { log('═══════════════════════════════════════════', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Notification received from: $source', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Raw data: $notificationData', name: 'CallNotificationService'); - - // Extract call information - FIXED: Match actual backend field names final notificationType = notificationData['notificationType'] as String? ?? notificationData['type'] as String?; // Backend uses 'type' final transactionType = notificationData['transactionType'] as String?; - log('📞 [INCOMING CALL] Notification Type: $notificationType', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Transaction Type: $transactionType', name: 'CallNotificationService'); diff --git a/lib/modules/cx_module/chat/call/video_call_page.dart b/lib/modules/cx_module/chat/call/video_call_page.dart index 3e256a84..823e4bc4 100644 --- a/lib/modules/cx_module/chat/call/video_call_page.dart +++ b/lib/modules/cx_module/chat/call/video_call_page.dart @@ -2,19 +2,14 @@ import 'dart:async'; import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; -import 'package:provider/provider.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; -import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; +import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; import 'package:test_sa/modules/cx_module/chat/model/call_session.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_themes.dart'; -import 'package:test_sa/modules/cx_module/chat/call/widgets/draggable_local_preview.dart'; class VideoCallPage extends StatefulWidget { const VideoCallPage({Key? key}) : super(key: key); @@ -24,33 +19,25 @@ class VideoCallPage extends StatefulWidget { } class _VideoCallPageState extends State { + late CallManager _callManager; Timer? _streamCheckTimer; bool _isLocalVideoFullscreen = false; @override void initState() { super.initState(); + _callManager = CallManager(); - if (kDebugMode) { - log('🎬 [VIDEO CALL PAGE] Page initialized', name: 'VideoCallPage'); - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - final chatProvider = context.read(); - - // Add listener to detect when call ends - chatProvider.addListener(_checkCallStatus); + // Listen for call end + _callManager.addListener(_checkCallStatus); - // Start periodic check to ensure remote stream gets assigned - _startStreamCheckTimer(); - }); + // Start periodic check to ensure remote stream gets assigned + _startStreamCheckTimer(); } void _startStreamCheckTimer() { - // Check every 500ms if remote stream needs to be assigned _streamCheckTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { - final chatProvider = context.read(); - final webrtc = chatProvider.webrtcService; + final webrtc = _callManager.webrtcService; if (webrtc == null || !mounted) { timer.cancel(); @@ -63,58 +50,32 @@ class _VideoCallPageState extends State { webrtc.remoteRenderer!.srcObject == null) { try { webrtc.remoteRenderer!.srcObject = webrtc.remoteStream; - - // Force UI rebuild - if (mounted) { - setState(() {}); - } + if (mounted) setState(() {}); } catch (e) { if (kDebugMode) { - log('⚠️ [VIDEO CALL PAGE] Stream assignment failed: $e', name: 'VideoCallPage'); + log('⚠️ Stream assignment failed: $e', name: 'VideoCallPage'); } } } // If call is connected and remote stream is assigned, slow down checks - if (chatProvider.callStatus == CallStatus.connected && + if (_callManager.callStatus == CallStatus.connected && webrtc.remoteRenderer?.srcObject != null) { timer.cancel(); - - // Switch to slower monitoring (every 5 seconds) - Timer.periodic(const Duration(seconds: 5), (slowTimer) { - if (!mounted) { - slowTimer.cancel(); - } - }); } }); } void _checkCallStatus() { - final chatProvider = context.read(); - - // If call ended, navigate back - if (chatProvider.callStatus == CallStatus.idle && mounted) { - chatProvider.removeListener(_checkCallStatus); + if (_callManager.callStatus == CallStatus.idle && mounted) { Navigator.of(context).pop(); } } @override void dispose() { - // Cancel stream check timer _streamCheckTimer?.cancel(); - _streamCheckTimer = null; - - // Clean up listener - try { - context.read().removeListener(_checkCallStatus); - } catch (e) { - // Provider might already be disposed - if (kDebugMode) { - log('⚠️ [VIDEO CALL PAGE] Error removing listener: $e', name: 'VideoCallPage'); - } - } + _callManager.removeListener(_checkCallStatus); super.dispose(); } @@ -124,25 +85,29 @@ class _VideoCallPageState extends State { backgroundColor: AppColor.backgroundTabBarDark, body: SafeArea( top: false, - child: Selector( - selector: (_, provider) => provider.currentCall, - builder: (context, session, _) { + child: ListenableBuilder( + listenable: _callManager, + builder: (context, _) { + final session = _callManager.currentCall; if (session == null) { return const Center( - child: Text( - 'No active call', - style: TextStyle(color: Colors.white), - ), + child: Text('No active call', style: TextStyle(color: Colors.white)), ); } return Stack( children: [ - // Background video (fullscreen) - _isLocalVideoFullscreen ? _buildFullscreenLocalVideo(context) : _buildRemoteVideo(context), + // Remote video (fullscreen by default) + if (!_isLocalVideoFullscreen) _buildRemoteVideo(context), + + // Local video (fullscreen when tapped) + if (_isLocalVideoFullscreen) _buildFullscreenLocalVideo(context), - // Small preview video (top-right corner) - _isLocalVideoFullscreen ? _buildSmallRemoteVideo(context) : _buildLocalVideo(context), + // Small preview (top-right corner) + if (_isLocalVideoFullscreen) + _buildSmallRemoteVideo(context) + else + _buildLocalVideoPreview(context), _buildTopBar(context, session), _buildBottomControls(context, session), @@ -154,419 +119,174 @@ class _VideoCallPageState extends State { ); } - // Fullscreen local video (when tapped) - Widget _buildFullscreenLocalVideo(BuildContext context) { - final chatProvider = context.read(); + Widget _buildRemoteVideo(BuildContext context) { + final webrtc = _callManager.webrtcService; + final isPeerCameraOn = _callManager.isPeerCameraOn; return Positioned.fill( child: GestureDetector( - onTap: () { - setState(() { - _isLocalVideoFullscreen = false; // Switch back to remote fullscreen - }); - }, - child: Consumer( - builder: (context, chatProvider, _) { - if (!chatProvider.isCameraOn) { - // Show avatar when camera is off - return Container( - color: Colors.grey[800], - child: const Center( - child: Icon( - Icons.videocam_off, - color: Colors.white, - size: 64, - ), + onTap: () => setState(() => _isLocalVideoFullscreen = true), + child: Container( + color: Colors.black, + child: !isPeerCameraOn || webrtc?.remoteRenderer == null + ? _buildAvatarPlaceholder(_callManager.currentCall?.peerName ?? 'User', large: true) + : RTCVideoView( + webrtc!.remoteRenderer!, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, ), - ); - } - - if (chatProvider.webrtcService?.localRenderer != null) { - return RTCVideoView( - chatProvider.webrtcService!.localRenderer!, - objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, - mirror: true, - ); - } - - return Container( - color: Colors.black, - child: const Center( - child: CircularProgressIndicator(color: Colors.white54), - ), - ); - }, ), ), ); } - // Small remote video (when local is fullscreen) - Widget _buildSmallRemoteVideo(BuildContext context) { - return Positioned( - top: MediaQuery.of(context).padding.top + 80, - right: 16, - child: GestureDetector( - onTap: () { - setState(() { - _isLocalVideoFullscreen = false; // Switch back to remote fullscreen - }); - }, - child: Consumer( - builder: (context, chatProvider, _) { - final remoteStream = chatProvider.webrtcService?.remoteStream; - final remoteRenderer = chatProvider.webrtcService?.remoteRenderer; - - if (!chatProvider.isPeerCameraOn) { - // Show avatar placeholder - return Container( - width: 120, - height: 160, - decoration: BoxDecoration( - color: AppColor.backgroundTabBarDark, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 2), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.videocam_off, color: Colors.white, size: 32), - 8.height, - Text( - chatProvider.currentCall?.peerName.split(' ').first ?? '', - style: AppTextStyles.bodyText2.copyWith( - color: Colors.white, - fontSize: 12, - ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ); - } - - if (remoteRenderer != null && remoteStream != null && remoteRenderer.srcObject != null) { - return Container( - width: 120, - height: 160, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 2), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: RTCVideoView( - remoteRenderer, - objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, - mirror: false, - ), - ), - ); - } - - return Container( - width: 120, - height: 160, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 2), - ), - child: const Center( - child: CircularProgressIndicator(color: Colors.white54, strokeWidth: 2), - ), - ); - }, - ), - ), - ); - } + Widget _buildFullscreenLocalVideo(BuildContext context) { + final webrtc = _callManager.webrtcService; + final isCameraOn = _callManager.isCameraOn; - Widget _buildRemoteVideo(BuildContext context) { return Positioned.fill( child: GestureDetector( - onTap: () { - setState(() { - _isLocalVideoFullscreen = true; // Switch to local fullscreen - }); - }, - child: Consumer( - builder: (context, chatProvider, _) { - final remoteRenderer = chatProvider.webrtcService?.remoteRenderer; - final remoteStream = chatProvider.webrtcService?.remoteStream; - - if (remoteRenderer != null) { - if (remoteRenderer.srcObject == null && remoteStream != null) { - // Renderer exists but srcObject is null, assign it - WidgetsBinding.instance.addPostFrameCallback((_) { - final renderer = chatProvider.webrtcService?.remoteRenderer; - final stream = chatProvider.webrtcService?.remoteStream; - if (renderer != null && stream != null && renderer.srcObject == null) { - renderer.srcObject = stream; - // Force UI rebuild - if (mounted) { - setState(() {}); - } - } - }); - } - - // If renderer has stream OR we just assigned it, show the video view - final hasRendererStream = remoteRenderer.srcObject != null; - - if (hasRendererStream) { - return RTCVideoView( - remoteRenderer, + onTap: () => setState(() => _isLocalVideoFullscreen = false), + child: Container( + color: Colors.grey[800], + child: !isCameraOn || webrtc?.localRenderer == null + ? const Center(child: Icon(Icons.videocam_off, color: Colors.white, size: 64)) + : RTCVideoView( + webrtc!.localRenderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, - mirror: false, - ); - } - } - - // Fallback while waiting for remote stream - return Container( - color: Colors.black, - child: const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(color: Colors.white54), - SizedBox(height: 16), - Text( - 'Waiting for video...', - style: TextStyle(color: Colors.white54), - ), - ], + mirror: true, ), - ), - ); - }, ), ), ); } - Widget _buildRemoteAvatarPlaceholder(BuildContext context) { - return Selector( - selector: (_, provider) => provider.currentCall, - builder: (context, session, _) { - if (session == null) return const SizedBox.shrink(); + Widget _buildLocalVideoPreview(BuildContext context) { + final webrtc = _callManager.webrtcService; + final isCameraOn = _callManager.isCameraOn; - return Container( - color: AppColor.backgroundTabBarDark, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Stack( - alignment: Alignment.center, - children: [ - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.whiteF8d, - border: Border.all(color: AppColor.white10.withAlpha(51), width: 1), - ), - child: ClipOval( - child: session.peerAvatar != null - ? CachedNetworkImage( - imageUrl: session.peerAvatar!, - fit: BoxFit.cover, - placeholder: (context, url) => Center( - child: CircularProgressIndicator( - color: Colors.white.withAlpha(128), - ), - ), - errorWidget: (context, url, error) => 'call_user_avatar'.toSvgAsset(height: 48, width: 48), - ) - : 'call_user_avatar'.toSvgAsset(height: 48, width: 48), - ), - ), - // Show mute indicator badge when peer is muted - Selector( - selector: (_, provider) => provider.isPeerMuted, - builder: (context, isPeerMuted, _) { - if (!isPeerMuted) return const SizedBox.shrink(); - - return Positioned( - bottom: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColor.red30, - shape: BoxShape.circle, - border: Border.all(color: AppColor.white10, width: 2), - ), - child: 'mic_disable'.toSvgAsset( - width: 16, - height: 16, - color: AppColor.white10, - ), - ), - ); - }, - ), - ], - ), - 24.height, - Text( - session.peerName, - style: AppTextStyles.heading2.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - 8.height, - Text( - 'Camera is off', - style: AppTextStyles.heading5.copyWith( - color: AppColor.neutral100, - fontWeight: FontWeight.w400, - ), - ), - // Show mute status when peer is muted - Selector( - selector: (_, provider) => provider.isPeerMuted, - builder: (context, isPeerMuted, _) { - if (!isPeerMuted) return const SizedBox.shrink(); - - return Column( - children: [ - 8.height, - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: AppColor.red30.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - 'mic_disable'.toSvgAsset( - width: 14, - height: 14, - color: AppColor.white10, - ), - 6.width, - Text( - 'Microphone is off', - style: AppTextStyles.bodyText2.copyWith( - color: AppColor.white10, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ); - }, + return Positioned( + top: 100, + right: 16, + child: GestureDetector( + onTap: () => setState(() => _isLocalVideoFullscreen = true), + child: Container( + width: 120, + height: 160, + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white24, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: !isCameraOn || webrtc?.localRenderer == null + ? const Center(child: Icon(Icons.videocam_off, color: Colors.white, size: 32)) + : RTCVideoView( + webrtc!.localRenderer!, + objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, + mirror: true, ), - ], - ), - )); - }, + ), + ), + ), ); } - Widget _buildLocalVideo(BuildContext context) { - final chatProvider = context.read(); + Widget _buildSmallRemoteVideo(BuildContext context) { + final webrtc = _callManager.webrtcService; + final isPeerCameraOn = _callManager.isPeerCameraOn; return Positioned( - top: MediaQuery.of(context).padding.top + 80, + top: 100, right: 16, child: GestureDetector( - onTap: () { - setState(() { - _isLocalVideoFullscreen = true; // Make local video fullscreen - }); - }, - child: Selector( - selector: (_, provider) => provider.isCameraOn, - builder: (context, isCameraOn, _) { - if (!isCameraOn) { - return Container( - width: 120, - height: 160, - decoration: BoxDecoration( - color: Colors.grey[800], - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 2), - ), - child: const Icon( - Icons.videocam_off, - color: Colors.white, - size: 32, - ), - ); - } - - if (chatProvider.webrtcService?.localRenderer != null) { - return Container( - width: 120, - height: 160, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 2), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: RTCVideoView( - chatProvider.webrtcService!.localRenderer!, + onTap: () => setState(() => _isLocalVideoFullscreen = false), + child: Container( + width: 120, + height: 160, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white24, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: !isPeerCameraOn || webrtc?.remoteRenderer == null + ? _buildAvatarPlaceholder(_callManager.currentCall?.peerName ?? 'User', large: false) + : RTCVideoView( + webrtc!.remoteRenderer!, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover, - mirror: true, ), - ), - ); - } + ), + ), + ), + ); + } - return Container( - width: 120, - height: 160, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 2), + Widget _buildAvatarPlaceholder(String name, {required bool large}) { + return Container( + color: AppColor.backgroundTabBarDark, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.person, + size: large ? 80 : 40, + color: Colors.white54, + ), + if (large) ...[ + 12.height, + Text( + name, + style: AppTextStyles.heading5.copyWith(color: Colors.white), ), - child: const Center( - child: CircularProgressIndicator(color: Colors.white54, strokeWidth: 2), + 4.height, + Text( + 'Camera is off', + style: AppTextStyles.bodyText2.copyWith(color: Colors.white54), ), - ); - }, + ], + ], ), ), ); } Widget _buildTopBar(BuildContext context, CallSession session) { + final status = _callManager.callStatus; + final duration = _callManager.callDuration; + + String statusText; + if (status == CallStatus.connected) { + final minutes = duration.inMinutes; + final seconds = duration.inSeconds % 60; + statusText = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } else { + statusText = _getStatusText(status); + } + return Positioned( top: 0, left: 0, right: 0, child: Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 16, + bottom: 16, + left: 16, + right: 16, + ), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Colors.black.withOpacity(0.7), + Colors.black.withOpacity(0.6), Colors.transparent, ], ), ), - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + 8, - left: 16, - right: 16, - bottom: 24, - ), child: Row( children: [ IconButton( @@ -578,13 +298,13 @@ class _VideoCallPageState extends State { children: [ Text( session.peerName, - style: AppTextStyles.bodyText2.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, - ), + style: AppTextStyles.heading6.copyWith(color: Colors.white), ), 4.height, - _buildCallStatusAndDuration(), + Text( + statusText, + style: AppTextStyles.bodyText2.copyWith(color: Colors.white70), + ), ], ), ), @@ -595,266 +315,90 @@ class _VideoCallPageState extends State { ); } - Widget _buildCallStatusAndDuration() { - return Selector( - selector: (_, provider) => provider.callDuration, - builder: (context, duration, _) { - if (duration.inSeconds == 0) { - return Selector( - selector: (_, provider) => provider.callStatus, - builder: (context, status, _) { - String statusText; - switch (status) { - case CallStatus.outgoingRinging: - statusText = 'Calling...'; - break; - case CallStatus.connecting: - statusText = 'Connecting...'; - break; - default: - statusText = ''; - } - return Text( - statusText, - style: AppTextStyles.bodyText.copyWith( - color: Colors.white70, - ), - ); - }, - ); - } - - String twoDigits(int n) => n.toString().padLeft(2, '0'); - final minutes = twoDigits(duration.inMinutes.remainder(60)); - final seconds = twoDigits(duration.inSeconds.remainder(60)); - - return Text( - '$minutes:$seconds', - style: AppTextStyles.bodyText.copyWith( - color: Colors.white, - fontFeatures: [const FontFeature.tabularFigures()], - ), - ); - }, - ); + String _getStatusText(CallStatus status) { + switch (status) { + case CallStatus.connecting: + return 'Connecting...'; + case CallStatus.incomingRinging: + return 'Incoming call...'; + case CallStatus.outgoingRinging: + return 'Ringing...'; + case CallStatus.connected: + return 'Connected'; + default: + return ''; + } } Widget _buildBottomControls(BuildContext context, CallSession session) { - final chatProvider = context.read(); + final isMuted = _callManager.isMuted; + final isCameraOn = _callManager.isCameraOn; + final isSpeakerOn = _callManager.isSpeakerOn; + return Positioned( - bottom: 0, + bottom: 40, left: 0, right: 0, child: Container( - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Color.fromRGBO(65, 67, 82, 0.7), - Color.fromRGBO(105, 109, 133, 0.7), - Color.fromRGBO(65, 67, 82, 0.7), - ], - stops: [0.0, 0.5, 1.0], - ), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.white10.withAlpha(51)), - ), - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - // Name with mute indicator on the right - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - session.peerName, - style: AppTextStyles.heading2.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - // Mute indicator badge (top-right of name) - Selector( - selector: (_, provider) => provider.isPeerMuted, - builder: (context, isPeerMuted, _) { - if (!isPeerMuted) return const SizedBox.shrink(); - - return Padding( - padding: const EdgeInsets.only(left: 8), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: AppColor.red30, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColor.white10.withOpacity(0.3), width: 1), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - 'mic_disable'.toSvgAsset( - width: 14, - height: 14, - color: AppColor.white10, - ), - 4.width, - Text( - 'Muted', - style: AppTextStyles.bodyText2.copyWith( - color: AppColor.white10, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ); - }, - ), - ], + _buildControlButton( + icon: isMuted ? Icons.mic_off : Icons.mic, + label: isMuted ? 'Unmute' : 'Mute', + onPressed: () => _callManager.toggleMute(), + backgroundColor: isMuted ? Colors.red : Colors.white24, ), - 16.height, - _buildCallStatusOrDuration(context), - 16.height, - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Selector( - selector: (_, provider) => provider.isMuted, - builder: (context, isMuted, _) { - return _buildVideoControlButton( - icon: isMuted ? 'mic_disable' : 'mic_enable', - isActive: isMuted, - onPressed: () => chatProvider.toggleMute(), - ); - }, - ), - - // Camera toggle - Selector( - selector: (_, provider) => provider.isCameraOn, - builder: (context, isCameraOn, _) { - return _buildVideoControlButton( - icon: isCameraOn ? 'video_enable' : 'video_disable', - isActive: !isCameraOn, - onPressed: () => chatProvider.toggleCamera(), - ); - }, - ), - - // Switch camera - _buildVideoControlButton( - icon: 'toggle_camera', - isActive: false, - onPressed: () => chatProvider.switchCamera(), - ), - - // Speaker - Selector( - selector: (_, provider) => provider.isSpeakerOn, - builder: (context, isSpeakerOn, _) { - return _buildVideoControlButton( - icon: 'speaker_enable', - isActive: isSpeakerOn, - onPressed: () => chatProvider.toggleSpeaker(), - ); - }, - ), - _buildVideoControlButton( - icon: 'end_call', - isHangUp: true, - showBorder: false, - onPressed: () { - chatProvider.hangUp(); - // Removed Navigator.pop() - automatic listener will handle navigation - }, - ), - ], + _buildControlButton( + icon: isCameraOn ? Icons.videocam : Icons.videocam_off, + label: isCameraOn ? 'Camera' : 'Camera Off', + onPressed: () => _callManager.toggleCamera(), + backgroundColor: isCameraOn ? Colors.white24 : Colors.red, + ), + _buildControlButton( + icon: Icons.cameraswitch, + label: 'Switch', + onPressed: () => _callManager.switchCamera(), + backgroundColor: Colors.white24, + ), + _buildControlButton( + icon: Icons.call_end, + label: 'End', + onPressed: () => _callManager.hangUp(), + backgroundColor: Colors.red, + iconColor: Colors.white, ), ], ), - ).paddingOnly(bottom: 16, start: 16, end: 16), - ); - } - - Widget _buildCallStatusOrDuration(BuildContext context) { - return Selector( - selector: (_, provider) => provider.callStatus, - builder: (context, status, _) { - String statusText; - switch (status) { - case CallStatus.outgoingRinging: - statusText = 'Calling...'; - break; - case CallStatus.incomingRinging: - statusText = 'Incoming call...'; - break; - case CallStatus.connecting: - statusText = 'Connecting...'; - break; - case CallStatus.connected: - return _buildDuration(context); - default: - statusText = ''; - } - return Text( - statusText, - style: AppTextStyles.heading5.copyWith( - color: AppColor.neutral100, - fontWeight: FontWeight.w400, - ), - ); - }, - ); - } - - Widget _buildDuration(BuildContext context) { - return Selector( - selector: (_, provider) => provider.callDuration, - builder: (context, duration, _) { - if (duration.inSeconds == 0) { - return const SizedBox.shrink(); - } - - String twoDigits(int n) => n.toString().padLeft(2, '0'); - final minutes = twoDigits(duration.inMinutes.remainder(60)); - final seconds = twoDigits(duration.inSeconds.remainder(60)); - - return Text( - '$minutes:$seconds', - style: AppTextStyles.heading5.copyWith( - color: AppColor.neutral100, - fontWeight: FontWeight.w400, - fontFeatures: [const FontFeature.tabularFigures()], - ), - ); - }, + ), ); } - Widget _buildVideoControlButton({ - required String icon, - bool? isActive, - bool isHangUp = false, - bool showBorder = true, + Widget _buildControlButton({ + required IconData icon, + required String label, required VoidCallback onPressed, + Color? backgroundColor, + Color? iconColor, }) { - return Container( - padding: const EdgeInsets.all(18), - decoration: BoxDecoration( - color: isHangUp - ? AppColor.red30 - : isActive != null && isActive - ? AppColor.primary10 - : AppColor.black2E, - shape: BoxShape.circle, - border: showBorder ? BoxBorder.all(color: AppColor.white10.withOpacity(0.2)) : null), - child: icon.toSvgAsset(width: 24, height: 24, color: AppColor.white10), - ).onPress(() { - onPressed(); - }); + return Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: backgroundColor ?? Colors.white24, + shape: BoxShape.circle, + border: Border.all(color: Colors.white24), + ), + child: Icon(icon, size: 24, color: iconColor ?? Colors.white), + ).onPress(onPressed), + 8.height, + Text( + label, + style: AppTextStyles.bodyText2.copyWith(color: Colors.white), + ), + ], + ); } } diff --git a/lib/modules/cx_module/chat/chat_page.dart b/lib/modules/cx_module/chat/chat_page.dart index d7693368..fa19ee03 100644 --- a/lib/modules/cx_module/chat/chat_page.dart +++ b/lib/modules/cx_module/chat/chat_page.dart @@ -190,7 +190,8 @@ class _ChatPageState extends State { @override void dispose() { - chatHubConnection?.stop(); + final chatProvider = Provider.of(context, listen: false); + chatProvider.chatHubConnection?.stop(); playerController.dispose(); recorderController.dispose(); super.dispose(); @@ -419,13 +420,13 @@ class _ChatPageState extends State { textInputAction: TextInputAction.none, keyboardType: TextInputType.multiline, onTap: () { - chatHubConnection!.invoke("SendTypingAsync", args: [receiver]); + chatProvider.chatHubConnection!.invoke("SendTypingAsync", args: [receiver]); }, onTapOutside: (PointerDownEvent event) { - chatHubConnection!.invoke("SendStopTypingAsync", args: [receiver]); + chatProvider.chatHubConnection!.invoke("SendStopTypingAsync", args: [receiver]); }, onChanged: (text) { - chatHubConnection!.invoke("SendTypingAsync", args: [receiver]); + chatProvider.chatHubConnection!.invoke("SendTypingAsync", args: [receiver]); }, decoration: InputDecoration( enabledBorder: InputBorder.none, diff --git a/lib/modules/cx_module/chat/chat_provider.dart b/lib/modules/cx_module/chat/chat_provider.dart index 78f0c86d..75828244 100644 --- a/lib/modules/cx_module/chat/chat_provider.dart +++ b/lib/modules/cx_module/chat/chat_provider.dart @@ -44,9 +44,8 @@ import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'call/call_error_handler.dart'; import 'call/services/call_notification_service.dart'; import 'services/call_manager.dart'; -import 'services/signalr_service.dart'; // ADD: Import SignalRService - -HubConnection? chatHubConnection; +import 'services/signalr_service.dart'; +import 'package:test_sa/core/di/service_locator.dart'; class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { bool isTyping = false; @@ -72,6 +71,21 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { late int moduleID; int? referenceID; + // SignalR Service - Retrieved from GetIt (Dependency Injection) + // DO NOT instantiate directly - always use getIt() + SignalRService get _signalRService => getIt(); + + // FIXED: Make chatHubConnection a regular property that can be set + HubConnection? _chatHubConnection; + + // Getter for backward compatibility + HubConnection? get chatHubConnection => _chatHubConnection ?? _signalRService.hubConnection; + + // Setter for backward compatibility + set chatHubConnection(HubConnection? connection) { + _chatHubConnection = connection; + } + // === CALL STATE - DELEGATED TO CallManager === // These getters delegate to CallManager for backwards compatibility CallStatus get callStatus => CallManager().callStatus; @@ -330,545 +344,87 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { /// Prevents connection leaks if initialization fails Future buildHubConnection(String conversationID) async { try { - // Dispose existing connection if any - await _disposeConnection(); - - chatHubConnection = await getHubConnection(); - await chatHubConnection!.start(); - - // NEW: Log connection details for diagnostics log('═══════════════════════════════════════════', name: 'ChatProvider'); - log('🔌 SignalR Hub Connection: Started', name: 'ChatProvider'); - log('🔑 Connection ID: ${chatHubConnection!.connectionId ?? "NULL"}', name: 'ChatProvider'); - log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider'); + log('🔌 [ChatProvider] buildHubConnection called', name: 'ChatProvider'); log('💬 Conversation ID: $conversationID', name: 'ChatProvider'); - log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider'); - log('═══════════════════════════════════════════', name: 'ChatProvider'); - - if (kDebugMode) { - print("🔌 SignalR Hub Connection: Started"); - } - - await chatHubConnection!.invoke("JoinConversation", args: [conversationID]); - log('✅ Joined conversation: $conversationID', name: 'ChatProvider'); - - // CRITICAL: Share this connection with SignalRService for CallManager - log('🔗 [SIGNALR] Sharing connection with CallManager...', name: 'ChatProvider'); - SignalRService().useExistingConnection( - chatHubConnection!, - userId: chatLoginResponse!.userId.toString(), - authToken: chatLoginResponse!.token ?? '', - conversationId: conversationID, - ); - log('✅ [SIGNALR] Connection shared with CallManager', name: 'ChatProvider'); - - // Register ALL event handlers (chat + call) - _registerAllEventHandlers(); - - //group On message - // chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); - } catch (e) { - log('❌ [CONNECTION] Error building SignalR connection: $e', name: 'ChatProvider'); - if (kDebugMode) { - print('⚠️ Error building SignalR connection: $e'); - } - // Clean up on error - await _disposeConnection(); - rethrow; // Rethrow so caller knows about the error - } - } - - /// Register all event handlers (chat + call) - called on initial connection and after reconnect - void _registerAllEventHandlers() { - if (chatHubConnection == null) { - log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider'); - return; - } - - log('🔧 [EVENT HANDLERS] Registering all event handlers...', name: 'ChatProvider'); - - // Register chat event handlers - chatHubConnection!.on("ReceiveMessage", onMsgReceived1); - chatHubConnection!.on("OnMessageReceivedAsync", onMsgReceived); - chatHubConnection!.on("OnSubmitChatAsync", onSubmitChatAsync); - chatHubConnection!.on("OnTypingAsync", OnTypingAsync); - chatHubConnection!.on("OnStopTypingAsync", OnStopTypingAsync); - chatHubConnection!.on("OnSeenChatUserAsync", onSeenUserChatAsync); - chatHubConnection!.on("OnAckSeenAsync", onAckSeenAsync); - - // Register call history event handler - chatHubConnection!.on("OnCallHistoryUpdated", _onCallHistoryUpdated); - - // Register call event handlers - _registerCallHandlers(); - - log('✅ [EVENT HANDLERS] All event handlers registered successfully', name: 'ChatProvider'); - } - - Future getHubConnection() async { - if (kDebugMode) { - print('🔧 Creating new SignalR hub connection...'); - } - - HubConnection hub; - HttpConnectionOptions httpOp = HttpConnectionOptions( - skipNegotiation: false, - logMessageContent: true, - ); - - hub = HubConnectionBuilder() - .withUrl("${URLs.chatHubUrlChat}?UserId=${chatLoginResponse!.userId}&source=Desktop&access_token=${chatLoginResponse!.token}", options: httpOp) - .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]).build(); - - // NEW: Add global event logging in debug mode AND re-register handlers on reconnect - if (kDebugMode) { - // Log connection state changes - hub.onclose(({Exception? error}) { - log('🔴 [SignalR] Connection closed: $error', name: 'ChatProvider'); - }); - - hub.onreconnecting(({Exception? error}) { - log('🟡 [SignalR] Reconnecting: $error', name: 'ChatProvider'); - }); - - hub.onreconnected(({String? connectionId}) async { - log('🟢 [SignalR] Reconnected: $connectionId', name: 'ChatProvider'); - log('🔄 [SignalR] Re-registering all event handlers after reconnection...', name: 'ChatProvider'); - - // CRITICAL: Re-register all event handlers after reconnection - _registerAllEventHandlers(); - - log('✅ [SignalR] Event handlers re-registered after reconnection', name: 'ChatProvider'); - }); - - print('✅ SignalR hub connection created with debug logging and auto-reregister'); - } else { - // Production: still need to re-register handlers on reconnect - hub.onreconnected(({String? connectionId}) async { - _registerAllEventHandlers(); - }); - print('✅ SignalR hub connection created'); - } - - return hub; - } - - void registerEvents() { - // chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus); - // chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); - // chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); - // chatHubConnection.on("OnUserTypingAsync", onUserTyping); - chatHubConnection?.on("OnUserCountAsync", userCountAsync); - // chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); - // chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); - // chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); - // chatHubConnection.on("OnGetGroupUserStatusAsync", getGroupUserStatus); + // Use SignalR service from DI + final signalRService = _signalRService; - // - // {"type":1,"target":"","arguments":[[{"id":217869,"userName":"Sultan.Khan","email":"Sultan.Khan@cloudsolutions.com.sa","phone":null,"title":"Sultan.Khan","userStatus":1,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":false,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null},{"id":15153,"userName":"Tamer.Fanasheh","email":"Tamer.F@cloudsolutions.com.sa","phone":null,"title":"Tamer Fanasheh","userStatus":2,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":true,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null}]]} + log('🔍 [ChatProvider] SignalR service instance: ${signalRService.hashCode}', name: 'ChatProvider'); + log(' Is connected: ${signalRService.isConnected}', name: 'ChatProvider'); + log(' Connection state: ${signalRService.connectionState}', name: 'ChatProvider'); - if (kDebugMode) { - // logger.i("All listeners registered"); - } - } - - // Future getUserRecentChats() async { - // ChatUserModel recentChat = await ChatApiClient().getRecentChats(); - // ChatUserModel favUList = await ChatApiClient().getFavUsers(); - // // userGroups = await ChatApiClient().getGroupsByUserId(); - // if (favUList.response != null && recentChat.response != null) { - // favUsersList = favUList.response!; - // favUsersList.sort((ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo(b.userName!.toLowerCase())); - // for (dynamic user in recentChat.response!) { - // for (dynamic favUser in favUList.response!) { - // if (user.id == favUser.id) { - // user.isFav = favUser.isFav; - // } - // } - // } - // } - // pChatHistory = recentChat.response ?? []; - // uGroups = userGroups.groupresponse ?? []; - // pChatHistory!.sort((ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo(b.userName!.toLowerCase())); - // searchedChats = pChatHistory; - // isLoading = false; - // await invokeUserChatHistoryNotDeliveredAsync(userId: int.parse(AppState().chatDetails!.response!.id.toString())); - // sort(); - // notifyListeners(); - // if (searchedChats!.isNotEmpty || favUsersList.isNotEmpty) { - // getUserImages(); - // } - // } + // Initialize SignalR if not already connected + if (!signalRService.isConnected) { + log('🔌 [ChatProvider] Initializing SignalR connection...', name: 'ChatProvider'); - Future invokeUserChatHistoryNotDeliveredAsync({required int userId}) async { - await chatHubConnection!.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]); - return ""; - } - - // void getSingleUserChatHistory({required int senderUID, required int receiverUID, required bool loadMore, bool isNewChat = false}) async { - // isLoading = true; - // if (isNewChat) userChatHistory = []; - // if (!loadMore) paginationVal = 0; - // isChatScreenActive = true; - // receiverID = receiverUID; - // Response response = await ChatApiClient().getSingleUserChatHistory(senderUID: senderUID, receiverUID: receiverUID, loadMore: loadMore, paginationVal: paginationVal); - // if (response.statusCode == 204) { - // if (isNewChat) { - // userChatHistory = []; - // } else if (loadMore) {} - // } else { - // if (loadMore) { - // List temp = getSingleUserChatModel(response.body).reversed.toList(); - // userChatHistory.addAll(temp); - // } else { - // userChatHistory = getSingleUserChatModel(response.body).reversed.toList(); - // } - // } - // isLoading = false; - // notifyListeners(); - // - // if (isChatScreenActive && receiverUID == receiverID) { - // markRead(userChatHistory, receiverUID); - // } - // - // generateConvId(); - // } - // - // void generateConvId() async { - // Uuid uuid = const Uuid(); - // chatCID = uuid.v4(); - // } + final connected = await signalRService.initialize( + userId: chatLoginResponse!.userId.toString(), + authToken: chatLoginResponse!.token ?? '', + conversationId: conversationID, + ); - void markRead(List data, String receiverID) { - for (SingleUserChatModel element in data) { - // if (AppState().chatDetails!.response!.id! == element.targetUserId) { - if (element.isSeen != null) { - if (!element.isSeen!) { - element.isSeen = true; - dynamic data = [ - { - "userChatHistoryId": element.userChatHistoryId, - "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId, - "isDelivered": true, - "isSeen": true, - } - ]; - updateUserChatHistoryStatusAsync(data); - notifyListeners(); + if (!connected) { + throw Exception('Failed to initialize SignalR connection'); } - // } - } - } - } - - Future resetCount({ - required int moduleId, - required int referenceNo, - String? userId, - }) async { - try { - return await ChatApiClient().resetCountApi(moduleId, referenceNo, userId); - } catch (e, stack) { - debugPrint('resetCount error: $e'); - rethrow; - } - } - void updateUserChatHistoryStatusAsync(List data) { - try { - chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); - } catch (e) { - throw e; - } - } - - void updateUserChatHistoryOnMsg(List data) { - try { - chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); - } catch (e) { - throw e; - } - } - - // List getSingleUserChatModel(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); - List getSingleUserChatModel(String str) { - final dynamic decodedJson = json.decode(str); - - // Check if the decoded JSON is already a List - if (decodedJson is List) { - return List.from(decodedJson.map((x) => SingleUserChatModel.fromJson(x))); - } - // If it's a Map (a single object), wrap it in a list - else if (decodedJson is Map) { - return [SingleUserChatModel.fromJson(decodedJson)]; - } - // Handle unexpected types - else { - throw const FormatException('Expected a JSON object or a list of JSON objects.'); - } - } - - // List getGroupChatHistoryAsync(String str) => - // List.from(json.decode(str).map((x) => groupchathistory.GetGroupChatHistoryAsync.fromJson(x))); - // - Future uploadAttachments(String userId, File file, String fileSource) async { - dynamic result; - try { - Map jsonData = { - "IsContextual": true.toString(), - "ModuleCode": moduleID.toString(), - "ReferenceId": referenceID.toString(), - "ReferenceType": "ticket", - "ConversationId": chatParticipantModel!.id.toString(), - "TargetUserId": receiverID, - "SendMessage": true.toString(), - }; - - Object? response = await ChatApiClient().uploadMedia(userId, file, fileSource, jsonData: jsonData); - if (response != null) { - result = response; + log('✅ [ChatProvider] SignalR initialized', name: 'ChatProvider'); + log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider'); } else { - result = []; - } - } catch (e) { - throw e; - } - return result; - } - - Future> getUnReadMessages(String employeeId) async { - // employeeId = "FMEngineer"; + log('✅ [ChatProvider] SignalR already connected', name: 'ChatProvider'); + log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider'); - try { - Response response = await ApiClient().getJsonForResponse( - "${URLs.unreadMessages}?retrieveAll=true", - headers: {'x-api-key': URLs.chatApiKey, 'x-employee-number': employeeId}, - ); - if (response.statusCode == 200) { - List data = jsonDecode(response.body); - return data.map((elemet) => UnReadMessage.fromJson(elemet)).toList(); - } else { - return []; + // Just join the conversation if needed + if (conversationID.isNotEmpty) { + await signalRService.invoke("JoinConversation", args: [conversationID]); + log('✅ [ChatProvider] Joined conversation: $conversationID', name: 'ChatProvider'); + } } - } catch (error) { - return []; - } - } - - // void updateUserChatStatus(List? args) { - // dynamic items = args!.toList(); - // for (var cItem in items[0]) { - // for (SingleUserChatModel chat in userChatHistory) { - // if (cItem["contantNo"].toString() == chat.contantNo.toString()) { - // chat.isSeen = cItem["isSeen"]; - // chat.isDelivered = cItem["isDelivered"]; - // } - // } - // } - // notifyListeners(); - // } - - void getGroupUserStatus(List? args) { - //note: need to implement this function... - print(args); - } - - Future markMessageAsRead(int messageId) async { - final senderId = sender?.userId; - if (senderId == null) return; - chatHubConnection?.invoke( - "SendMessageReadAsync", - args: [messageId, senderId], - ); - } - void onChatSeen(List? args) { - dynamic items = args!.toList(); - // for (var user in searchedChats!) { - // if (user.id == items.first["id"]) { - // user.userStatus = items.first["userStatus"]; - // } - // } - // notifyListeners(); - } - - void userCountAsync(List? args) { - dynamic items = args!.toList(); - // logger.d(items); - //logger.d("---------------------------------User Count Async -------------------------------------"); - //logger.d(items); - // for (var user in searchedChats!) { - // if (user.id == items.first["id"]) { - // user.userStatus = items.first["userStatus"]; - // } - // } - // notifyListeners(); - } + // Use the connection from SignalRService + chatHubConnection = signalRService.hubConnection; - // void updateChatHistoryWindow(List? args) { - // dynamic items = args!.toList(); - // if (kDebugMode) { - // logger.i("---------------------------------Update Chat History Windows Async -------------------------------------"); - // } - // logger.d(items); - // // for (var user in searchedChats!) { - // // if (user.id == items.first["id"]) { - // // user.userStatus = items.first["userStatus"]; - // // } - // // } - // // notifyListeners(); - // } - - // void chatNotDelivered(List? args) { - // dynamic items = args!.toList(); - // for (dynamic item in items[0]) { - // for (ChatUser element in searchedChats!) { - // if (element.id == item["currentUserId"]) { - // int? val = element.unreadMessageCount ?? 0; - // element.unreadMessageCount = val! + 1; - // } - // } - // } - // notifyListeners(); - // } - // - // void changeStatus(List? args) { - // dynamic items = args!.toList(); - // for (ChatUser user in searchedChats!) { - // if (user.id == items.first["id"]) { - // user.userStatus = items.first["userStatus"]; - // } - // } - // if (teamMembersList.isNotEmpty) { - // for (ChatUser user in teamMembersList!) { - // if (user.id == items.first["id"]) { - // user.userStatus = items.first["userStatus"]; - // } - // } - // } - // - // notifyListeners(); - // } - // - // void filter(String value) async { - // List? tmp = []; - // if (value.isEmpty || value == "") { - // tmp = pChatHistory; - // } else { - // for (ChatUser element in pChatHistory!) { - // if (element.userName!.toLowerCase().contains(value.toLowerCase())) { - // tmp.add(element); - // } - // } - // } - // searchedChats = tmp; - // notifyListeners(); - // } - - Timer? timer; - - Future OnTypingAsync(List? parameters) async { - // String empId = parameters!.first as String; - isTyping = true; - notifyListeners(); - if (timer?.isActive ?? false) { - timer!.cancel(); - } - timer = Timer(const Duration(milliseconds: 2500), () { - isTyping = false; - notifyListeners(); - }); - } + // Log user details + log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider'); + log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider'); + log('═══════════════════════════════════════════', name: 'ChatProvider'); - Future OnStopTypingAsync(List? parameters) async { - if (timer?.isActive ?? false) { - timer!.cancel(); - } - isTyping = false; - notifyListeners(); - } + // Register chat event handlers + _registerChatEventHandlers(); - Future onSeenUserChatAsync(List? parameters) async { - try { - if (parameters == null || parameters.isEmpty) { - log('onSeenUserChatAsync: parameters are null or empty'); - return; - } - final parm = parameters.first; - if (parm is! List || parm.isEmpty) { - log('onSeenUserChatAsync: parm is not a valid list'); - return; - } - final firstItem = parm.first; - if (firstItem is! Map) { - log('onSeenUserChatAsync: firstItem is not a Map'); - return; - } - await chatHubConnection!.invoke( - "AckSeenAsync", - args: [ - firstItem['currentUserId'], - [firstItem['userChatHistoryId']], - ], - ); } catch (e, stackTrace) { - log('onSeenUserChatAsync error: $e'); - log('StackTrace: $stackTrace'); + log('❌ [ChatProvider] Error building SignalR connection: $e', + name: 'ChatProvider', error: e, stackTrace: stackTrace); + if (kDebugMode) { + print('⚠️ Error building SignalR connection: $e'); + } + // Clean up on error + await _disposeConnection(); + rethrow; } } - Future onAckSeenAsync(List? parameters) async { - try { - if (parameters == null || parameters.isEmpty) { - log('onAckSeenAsync: parameters are null or empty'); - return; - } - final parm = parameters.first; - log('parm onAckSeenAsync $parm'); - if (chatResponseList.isEmpty) { - log('onAckSeenAsync: chatResponseList is empty'); - return; - } - log('last list item id ${chatResponseList.first.toJson()}'); - chatResponseList.first.isSeen = true; - notifyListeners(); - } catch (e, stackTrace) { - log('onAckSeenAsync error: $e'); - log('StackTrace: $stackTrace'); + /// Register chat event handlers (separate from call handlers) + void _registerChatEventHandlers() { + if (chatHubConnection == null) { + log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider'); + return; } - } - - Future onSubmitChatAsync(List? parameters) async { - if (kDebugMode) print("OnSubmitChatAsync:$parameters"); - - if (parameters == null || parameters.isEmpty) return; - try { - for (dynamic msg in parameters) { - var data = getSingleUserChatModel(jsonEncode(msg)); - if (kDebugMode) print('Parsed message: $data'); - } - } catch (e) { - if (kDebugMode) print('Error in OnSubmitChatAsync: $e'); - } - } + log('🔧 [EVENT HANDLERS] Registering chat event handlers...', name: 'ChatProvider'); - Future onMsgReceived1(List? parameters) async { - print("onMsgReceived1:$parameters"); - } + // Register chat event handlers via SignalRService + _signalRService.on("ReceiveMessage", onMsgReceived1); + _signalRService.on("OnMessageReceivedAsync", onMsgReceived); + _signalRService.on("OnSubmitChatAsync", onSubmitChatAsync); + _signalRService.on("OnTypingAsync", OnTypingAsync); + _signalRService.on("OnStopTypingAsync", OnStopTypingAsync); + _signalRService.on("OnSeenChatUserAsync", onSeenUserChatAsync); + _signalRService.on("OnAckSeenAsync", onAckSeenAsync); + _signalRService.on("OnCallHistoryUpdated", _onCallHistoryUpdated); - Future onMsgReceived(List? parameters) async { - List data = []; - print("OnMessageReceivedAsync:$parameters"); - for (dynamic msg in parameters!) { - data = getSingleUserChatModel(jsonEncode(msg)); - // ...existing code... - } - // ...existing code... - userChatHistory?.insert(0, data.first); - notifyListeners(); - // ...existing code... + log('✅ [EVENT HANDLERS] Chat event handlers registered successfully', name: 'ChatProvider'); } // ==================== CALL INFRASTRUCTURE ==================== @@ -1002,4 +558,210 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { log('❌ [CALL HISTORY] Error reloading chat history: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace); } } + + // ==================== CHAT EVENT HANDLERS ==================== + + Future onMsgReceived1(List? parameters) async { + print("onMsgReceived1:$parameters"); + } + + Future onMsgReceived(List? parameters) async { + try { + if (parameters != null && parameters.isNotEmpty) { + var data = parameters[0] as Map; + SingleUserChatModel chatResponse = SingleUserChatModel.fromJson(Map.from(data)); + + // Add message to chat history + chatResponseList.insert(0, chatResponse); + chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!)); + notifyListeners(); + + if (kDebugMode) { + print('✅ Message received: ${chatResponse.contant}'); // Fixed: contant not message + } + } + } catch (e) { + if (kDebugMode) { + print('⚠️ Error in onMsgReceived: $e'); + } + } + } + + Future onSubmitChatAsync(List? parameters) async { + try { + if (parameters != null && parameters.isNotEmpty) { + var data = parameters[0] as Map; + SingleUserChatModel chatResponse = SingleUserChatModel.fromJson(Map.from(data)); + + // Update existing message or add new one + int existingIndex = chatResponseList.indexWhere((msg) => msg.userChatHistoryLineId == chatResponse.userChatHistoryLineId); // Fixed: use correct property + if (existingIndex != -1) { + chatResponseList[existingIndex] = chatResponse; + } else { + chatResponseList.insert(0, chatResponse); + } + + chatResponseList.sort((a, b) => b.createdDate!.compareTo(a.createdDate!)); + notifyListeners(); + + if (kDebugMode) { + print('✅ Chat submitted: ${chatResponse.contant}'); // Fixed: contant not message + } + } + } catch (e) { + if (kDebugMode) { + print('⚠️ Error in onSubmitChatAsync: $e'); + } + } + } + + Future OnTypingAsync(List? parameters) async { + try { + isTyping = true; + notifyListeners(); + + if (kDebugMode) { + print('✍️ User is typing...'); + } + } catch (e) { + if (kDebugMode) { + print('⚠️ Error in OnTypingAsync: $e'); + } + } + } + + Future OnStopTypingAsync(List? parameters) async { + try { + isTyping = false; + notifyListeners(); + + if (kDebugMode) { + print('✅ User stopped typing'); + } + } catch (e) { + if (kDebugMode) { + print('⚠️ Error in OnStopTypingAsync: $e'); + } + } + } + + Future onSeenUserChatAsync(List? parameters) async { + try { + if (parameters != null && parameters.isNotEmpty) { + // Handle message seen status update + if (kDebugMode) { + print('👁️ Messages marked as seen'); + } + notifyListeners(); + } + } catch (e) { + if (kDebugMode) { + print('⚠️ Error in onSeenUserChatAsync: $e'); + } + } + } + + Future onAckSeenAsync(List? parameters) async { + try { + if (parameters != null && parameters.isNotEmpty) { + // Handle message acknowledgment + if (kDebugMode) { + print('✅ Message acknowledgment received'); + } + notifyListeners(); + } + } catch (e) { + if (kDebugMode) { + print('⚠️ Error in onAckSeenAsync: $e'); + } + } + } + + // ==================== UTILITY METHODS ==================== + + /// Reset unread message count + Future resetCount({int? moduleId, int? referenceNo, String? userId}) async { // Fixed: userId is String? not int? + try { + if (chatLoginResponse != null && sender != null && recipient != null) { + await ChatApiClient().resetCountApi( + moduleId ?? moduleID, // Use parameter or fall back to stored value + referenceNo ?? referenceID ?? 0, + sender!.employeeNumber ?? '', + ); + + if (kDebugMode) { + print('✅ Reset unread count'); + } + return true; + } + return false; + } catch (e) { + if (kDebugMode) { + print('⚠️ resetCount error: $e'); + } + log('resetCount error: $e'); + return false; + } + } + + /// Upload attachments + Future?> uploadAttachments(String username, File file, String conversationId) async { // Fixed: match actual usage + try { + if (chatLoginResponse == null || chatParticipantModel == null) { + if (kDebugMode) { + print('⚠️ Cannot upload - chat not initialized'); + } + return null; + } + + // Upload single file + final files = [file]; + + // TODO: Implement file upload to chat API + // The ChatApiClient doesn't have uploadAttachments method yet + // For now, return empty list + if (kDebugMode) { + print('⚠️ uploadAttachments not implemented in ChatApiClient yet'); + print(' Username: $username'); + print(' File: ${file.path}'); + print(' Conversation: $conversationId'); + } + + return []; + } catch (e) { + if (kDebugMode) { + print('⚠️ Error uploading attachments: $e'); + } + log('uploadAttachments error: $e'); + return null; + } + } + + /// Get unread messages + Future> getUnReadMessages(String employeeId) async { // Fixed: accept employeeId and return List + try { + if (chatLoginResponse == null) { + if (kDebugMode) { + print('⚠️ Cannot get unread messages - not logged in'); + } + return []; + } + + // TODO: Implement getUnreadMessages API in ChatApiClient + // The ChatApiClient doesn't have this method yet + // For now, return empty list + if (kDebugMode) { + print('⚠️ getUnreadMessages API not implemented in ChatApiClient yet'); + print(' Requested for employeeId: $employeeId'); + } + + return []; + } catch (e) { + if (kDebugMode) { + print('⚠️ Error getting unread messages: $e'); + } + log('getUnReadMessages error: $e'); + return []; + } + } } diff --git a/lib/modules/cx_module/chat/services/call_manager.dart b/lib/modules/cx_module/chat/services/call_manager.dart index aee7ebae..4e0591ae 100644 --- a/lib/modules/cx_module/chat/services/call_manager.dart +++ b/lib/modules/cx_module/chat/services/call_manager.dart @@ -15,17 +15,20 @@ import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart'; import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:uuid/uuid.dart'; +import 'package:test_sa/core/di/service_locator.dart'; /// CallManager - Singleton service that manages all call responsibilities /// Works independently of ChatProvider -/// Uses SignalRService for SignalR communication +/// Uses SignalRService via Dependency Injection class CallManager extends ChangeNotifier { static final CallManager _instance = CallManager._internal(); factory CallManager() => _instance; - CallManager._internal(); + CallManager._internal() { + log('🏗️ [CallManager] Singleton instance created (hashCode: $hashCode)', name: 'CallManager'); + } - // Services - final SignalRService _signalRService = SignalRService(); + // Services - Retrieved from DI + SignalRService get _signalRService => getIt(); final CallKitService _callKitService = CallKitService(); WebRTCService? _webrtcService; @@ -996,7 +999,17 @@ class CallManager extends ChangeNotifier { log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager'); log('═══════════════════════════════════════════', name: 'CallManager'); + // CRITICAL: Navigate back to close call screen on both devices + final context = navigatorKey.currentContext; + if (context != null && Navigator.of(context).canPop()) { + log('🧭 [HANGUP] Navigating back to close call screen', name: 'CallManager'); + Navigator.of(context).pop(); + } + _cleanup(); + + log('✅ [HANGUP] Call ended and screen closed', name: 'CallManager'); + log('═══════════════════════════════════════════', name: 'CallManager'); } void _handleOffer(List? args) async { diff --git a/lib/modules/cx_module/chat/services/signalr_service.dart b/lib/modules/cx_module/chat/services/signalr_service.dart index eee0568d..2252dc72 100644 --- a/lib/modules/cx_module/chat/services/signalr_service.dart +++ b/lib/modules/cx_module/chat/services/signalr_service.dart @@ -5,28 +5,37 @@ import 'package:signalr_netcore/hub_connection.dart'; import 'package:signalr_netcore/signalr_client.dart'; import 'package:test_sa/controllers/api_routes/urls.dart'; -/// Singleton SignalR service that manages the single HubConnection -/// Both ChatProvider and CallManager use this service class SignalRService { + // Singleton instance - DO NOT instantiate directly, use DI (Provider) static final SignalRService _instance = SignalRService._internal(); factory SignalRService() => _instance; - SignalRService._internal(); + SignalRService._internal() { + log('🏗️ [SIGNALR] SignalRService singleton created (hashCode: ${hashCode})', name: 'SignalRService'); + } - // Single HubConnection instance + // Single HubConnection instance - ONLY created and managed by this service HubConnection? _hubConnection; - // Connection state + // Connection state management bool _isInitializing = false; + Completer? _initializationCompleter; // Authentication data String? _userId; String? _authToken; String? _currentConversationId; - // Event handler registrations + // Event handler registrations for re-registration after reconnect final Map?)>> _eventHandlers = {}; - /// Get the current HubConnection + // Connection state stream for reactive updates + final StreamController _connectionStateController = + StreamController.broadcast(); + + /// Stream of connection state changes for consumers to listen + Stream get connectionStateStream => _connectionStateController.stream; + + /// Get the current HubConnection (read-only access) HubConnection? get hubConnection => _hubConnection; /// Check if connected @@ -35,66 +44,47 @@ class SignalRService { /// Get current connection state HubConnectionState? get connectionState => _hubConnection?.state; - /// Use an existing HubConnection (from ChatProvider) - /// This prevents creating duplicate connections - void useExistingConnection(HubConnection existingConnection, { - String? userId, - String? authToken, - String? conversationId, - }) { - log('═══════════════════════════════════════════', name: 'SignalRService'); - log('🔌 [SIGNALR] Using existing HubConnection', name: 'SignalRService'); - log(' Connection ID: ${existingConnection.connectionId}', name: 'SignalRService'); - log(' User ID: ${userId ?? "not set"}', name: 'SignalRService'); - log(' Conversation ID: ${conversationId ?? "not set"}', name: 'SignalRService'); - log('═══════════════════════════════════════════', name: 'SignalRService'); - - _hubConnection = existingConnection; - _userId = userId; - _authToken = authToken; - _currentConversationId = conversationId; - - // Re-register all stored event handlers on this connection - _reregisterAllHandlers(); - } + /// Get connection ID (for debugging and verification) + String? get connectionId => _hubConnection?.connectionId; /// Initialize SignalR connection with authentication + /// Thread-safe: Multiple callers will wait for the same connection task Future initialize({ required String userId, required String authToken, String? conversationId, }) async { - try { - log('═══════════════════════════════════════════', name: 'SignalRService'); - log('🔌 [SIGNALR] Initializing SignalR connection...', name: 'SignalRService'); - log(' User ID: $userId', name: 'SignalRService'); - log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService'); - - // Prevent multiple simultaneous initializations - if (_isInitializing) { - log('⏳ [SIGNALR] Already initializing, waiting...', name: 'SignalRService'); - // Wait for current initialization to complete - final timeout = DateTime.now().add(const Duration(seconds: 5)); - while (_isInitializing && DateTime.now().isBefore(timeout)) { - await Future.delayed(const Duration(milliseconds: 100)); - } - return isConnected; - } - - // If already connected with same credentials, reuse connection - if (isConnected && _userId == userId && _authToken == authToken) { - log('✅ [SIGNALR] Already connected with same credentials, reusing connection', name: 'SignalRService'); + log('═══════════════════════════════════════════', name: 'SignalRService'); + log('🔌 [SIGNALR] initialize() called', name: 'SignalRService'); + log(' Instance hashCode: ${hashCode}', name: 'SignalRService'); + log(' User ID: $userId', name: 'SignalRService'); + log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService'); + + // THREAD SAFETY: If already initializing, wait for current initialization + if (_isInitializing && _initializationCompleter != null) { + log('⏳ [SIGNALR] Already initializing, waiting for completion...', name: 'SignalRService'); + return await _initializationCompleter!.future; + } - // Join new conversation if provided and different - if (conversationId != null && conversationId != _currentConversationId) { - await _joinConversation(conversationId); - } + // If already connected with same credentials, reuse connection + if (isConnected && _userId == userId && _authToken == authToken) { + log('✅ [SIGNALR] Already connected with same credentials', name: 'SignalRService'); + log(' Connection ID: ${_hubConnection?.connectionId}', name: 'SignalRService'); - return true; + // Join new conversation if provided and different + if (conversationId != null && conversationId != _currentConversationId) { + await _joinConversation(conversationId); } - _isInitializing = true; + log('═══════════════════════════════════════════', name: 'SignalRService'); + return true; + } + + // Start new initialization + _isInitializing = true; + _initializationCompleter = Completer(); + try { // Store credentials _userId = userId; _authToken = authToken; @@ -104,6 +94,8 @@ class SignalRService { await _disposeConnection(); // Create new connection + log('🔧 [SIGNALR] Creating new HubConnection...', name: 'SignalRService'); + final httpOp = HttpConnectionOptions( skipNegotiation: false, logMessageContent: kDebugMode, @@ -117,15 +109,21 @@ class SignalRService { .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]) .build(); - // Setup reconnection handlers + log('✅ [SIGNALR] HubConnection created (hashCode: ${_hubConnection.hashCode})', name: 'SignalRService'); + + // Setup reconnection handlers BEFORE starting connection _setupReconnectionHandlers(); // Start connection + log('🔌 [SIGNALR] Starting connection...', name: 'SignalRService'); await _hubConnection!.start(); log('✅ [SIGNALR] Connection established', name: 'SignalRService'); log(' Connection ID: ${_hubConnection!.connectionId}', name: 'SignalRService'); + // Emit connected state + _connectionStateController.add(HubConnectionState.Connected); + // Join conversation if provided if (conversationId != null) { await _joinConversation(conversationId); @@ -135,6 +133,7 @@ class SignalRService { _reregisterAllHandlers(); _isInitializing = false; + _initializationCompleter?.complete(true); log('═══════════════════════════════════════════', name: 'SignalRService'); return true; @@ -142,7 +141,11 @@ class SignalRService { } catch (e, stackTrace) { log('❌ [SIGNALR] Error initializing connection: $e', name: 'SignalRService', error: e, stackTrace: stackTrace); + _isInitializing = false; + _initializationCompleter?.complete(false); + log('═══════════════════════════════════════════', name: 'SignalRService'); + return false; } } @@ -153,14 +156,17 @@ class SignalRService { _hubConnection!.onclose(({Exception? error}) { log('🔴 [SIGNALR] Connection closed: $error', name: 'SignalRService'); + _connectionStateController.add(HubConnectionState.Disconnected); }); _hubConnection!.onreconnecting(({Exception? error}) { log('🟡 [SIGNALR] Reconnecting: $error', name: 'SignalRService'); + _connectionStateController.add(HubConnectionState.Reconnecting); }); _hubConnection!.onreconnected(({String? connectionId}) async { log('🟢 [SIGNALR] Reconnected: $connectionId', name: 'SignalRService'); + _connectionStateController.add(HubConnectionState.Connected); // Rejoin conversation if we had one if (_currentConversationId != null) { @@ -169,6 +175,8 @@ class SignalRService { // Re-register all event handlers _reregisterAllHandlers(); + + log('✅ [SIGNALR] Reconnection complete', name: 'SignalRService'); }); } @@ -189,6 +197,7 @@ class SignalRService { } /// Register an event handler + /// Events are stored and automatically re-registered after reconnect void on(String eventName, Function(List?) handler) { log('🔧 [SIGNALR] Registering handler for: $eventName', name: 'SignalRService'); @@ -196,11 +205,21 @@ class SignalRService { if (!_eventHandlers.containsKey(eventName)) { _eventHandlers[eventName] = []; } + + // Check for duplicate handler + if (_eventHandlers[eventName]!.contains(handler)) { + log('⚠️ [SIGNALR] Handler already registered for: $eventName', name: 'SignalRService'); + return; + } + _eventHandlers[eventName]!.add(handler); // Register with SignalR if connected if (_hubConnection != null) { _hubConnection!.on(eventName, handler); + log('✅ [SIGNALR] Handler registered for: $eventName', name: 'SignalRService'); + } else { + log('⚠️ [SIGNALR] Handler stored but not registered (not connected): $eventName', name: 'SignalRService'); } } @@ -227,18 +246,20 @@ class SignalRService { void _reregisterAllHandlers() { if (_hubConnection == null) return; - log('🔄 [SIGNALR] Re-registering ${_eventHandlers.length} event handlers...', name: 'SignalRService'); + log('🔄 [SIGNALR] Re-registering ${_eventHandlers.length} event types...', name: 'SignalRService'); + int totalHandlers = 0; for (final entry in _eventHandlers.entries) { final eventName = entry.key; final handlers = entry.value; for (final handler in handlers) { _hubConnection!.on(eventName, handler); + totalHandlers++; } } - log('✅ [SIGNALR] All event handlers re-registered', name: 'SignalRService'); + log('✅ [SIGNALR] $totalHandlers event handlers re-registered', name: 'SignalRService'); } /// Invoke a SignalR method @@ -248,16 +269,22 @@ class SignalRService { } log('📤 [SIGNALR] Invoking: $methodName', name: 'SignalRService'); + if (kDebugMode && args != null && args.isNotEmpty) { + log(' Args: ${args.take(3)}${args.length > 3 ? "..." : ""}', name: 'SignalRService'); + } + return await _hubConnection!.invoke(methodName, args: args); } /// Ensure connection is ready (connect if needed) + /// Thread-safe: Multiple callers will wait for the same connection task Future ensureConnected() async { if (isConnected) { return true; } if (_userId != null && _authToken != null) { + log('🔄 [SIGNALR] Not connected, attempting to reconnect...', name: 'SignalRService'); return await initialize( userId: _userId!, authToken: _authToken!, @@ -273,6 +300,7 @@ class SignalRService { Future _disposeConnection() async { try { if (_hubConnection != null) { + log('🔌 [SIGNALR] Disposing existing connection...', name: 'SignalRService'); await _hubConnection!.stop(); _hubConnection = null; log('✅ [SIGNALR] Connection disposed', name: 'SignalRService'); @@ -284,6 +312,7 @@ class SignalRService { /// Reset service (for logout) Future reset() async { + log('═══════════════════════════════════════════', name: 'SignalRService'); log('🔄 [SIGNALR] Resetting service...', name: 'SignalRService'); await _disposeConnection(); @@ -293,7 +322,19 @@ class SignalRService { _authToken = null; _currentConversationId = null; _isInitializing = false; + _initializationCompleter = null; log('✅ [SIGNALR] Service reset complete', name: 'SignalRService'); + log('═══════════════════════════════════════════', name: 'SignalRService'); + } + + /// Disconnect from SignalR (alias for reset) + Future disconnect() async { + await reset(); + } + + /// Dispose stream controller (called when app is terminating) + void dispose() { + _connectionStateController.close(); } } diff --git a/lib/new_views/pages/land_page/land_page.dart b/lib/new_views/pages/land_page/land_page.dart index bd475186..d22e5952 100644 --- a/lib/new_views/pages/land_page/land_page.dart +++ b/lib/new_views/pages/land_page/land_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer' as dev; import 'dart:io'; import 'package:flutter/material.dart'; @@ -23,6 +24,9 @@ import 'package:test_sa/new_views/pages/login_page.dart'; import 'package:test_sa/new_views/pages/settings_page.dart'; import 'package:test_sa/providers/work_order/vendor_provider.dart'; import 'package:test_sa/views/widgets/equipment/my_assets_page.dart'; +import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; +import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart'; +import 'package:test_sa/modules/cx_module/chat/chat_api_client.dart'; import '../../../controllers/providers/settings/setting_provider.dart'; import '../../../views/widgets/dialogs/dialog.dart'; @@ -124,19 +128,22 @@ class _LandPageState extends State { DashboardView(onDrawerPress: (() { _scaffoldKey.currentState!.isDrawerOpen ? _scaffoldKey.currentState!.closeDrawer() : _scaffoldKey.currentState!.openDrawer(); })), - // const old_page.LandPage(), const MyRequestsPage(), - // if (_userProvider!.user!.type != UsersTypes.engineer) const SizedBox(), - // if (_userProvider!.user!.type != UsersTypes.engineer) const CalendarPage(), const MyAssetsPage(fromBottomBar: true), ]; + if (isFCM) { FirebaseNotificationManger.initialized(context); NotificationManger.initialisation((notificationDetails) { FirebaseNotificationManger.handleMessage(context, json.decode(notificationDetails.payload!)); }, (id, title, body, payload) async {}); + // ⭐ Initialize SignalR & CallManager ONCE at app startup + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _initializeCallingSystem(); + }); + isFCM = false; } checkLocalAuth(); @@ -149,6 +156,9 @@ class _LandPageState extends State { builder: (_) => AAlertDialog(title: context.translation.signOut, content: context.translation.logoutAlert), ); if (result) { + // Cleanup calling system before logout + await _cleanupCallingSystem(); + bool isSuccess = await Provider.of(context, listen: false).logout(context); if (isSuccess) { Provider.of(context, listen: false).resetSettings(); @@ -209,4 +219,107 @@ class _LandPageState extends State { ), ); } + + /// Initialize SignalR and CallManager once at app startup + /// This ensures calls work from ANY screen without CMDetailPage dependency + Future _initializeCallingSystem() async { + try { + final user = _userProvider?.user; + if (user?.username == null) { + dev.log('⚠️ [LAND PAGE] User not logged in, skipping call initialization', name: 'LandPage'); + return; + } + + final myEmployeeNumber = user!.username!; // Use username as employee number + final userId = user.userID?.toString() ?? user.username!; + + dev.log('═══════════════════════════════════════════', name: 'LandPage'); + dev.log('🚀 [LAND PAGE] Initializing calling system...', name: 'LandPage'); + dev.log(' User: ${user.username}', name: 'LandPage'); + dev.log(' User ID: $userId', name: 'LandPage'); + dev.log(' Employee Number: $myEmployeeNumber', name: 'LandPage'); + + // Step 1: Get chat credentials + dev.log('🔑 [LAND PAGE] Fetching chat credentials...', name: 'LandPage'); + final chatLoginResponse = await ChatApiClient().getChatLoginToken( + 1002, // moduleId - using default + 0, // referenceId - using default for app-level init + 'App Init', + myEmployeeNumber, + myEmployeeNumber, + ); + + if (chatLoginResponse == null || chatLoginResponse.token == null) { + dev.log('❌ [LAND PAGE] Failed to get chat credentials', name: 'LandPage'); + dev.log(' Response: ${chatLoginResponse?.toJson()}', name: 'LandPage'); + return; + } + + dev.log('✅ [LAND PAGE] Got chat credentials', name: 'LandPage'); + dev.log(' User ID: ${chatLoginResponse.userId}', name: 'LandPage'); + dev.log(' Token: ${chatLoginResponse.token!.substring(0, 20)}...', name: 'LandPage'); + + // Step 2: Initialize SignalR Service (SINGLETON - only once) + dev.log('🔌 [LAND PAGE] Initializing SignalR Service...', name: 'LandPage'); + final signalRService = SignalRService(); + + final connected = await signalRService.initialize( + userId: chatLoginResponse.userId.toString(), + authToken: chatLoginResponse.token!, + conversationId: null, // No conversation at app level + ); + + if (!connected) { + dev.log('❌ [LAND PAGE] SignalR initialization failed', name: 'LandPage'); + return; + } + + dev.log('✅ [LAND PAGE] SignalR Service initialized', name: 'LandPage'); + dev.log(' Connection State: ${signalRService.connectionState}', name: 'LandPage'); + dev.log(' Connection ID: ${signalRService.connectionId ?? "NULL"}', name: 'LandPage'); + + // Step 3: Initialize CallManager (registers call event handlers) + dev.log('📞 [LAND PAGE] Initializing CallManager...', name: 'LandPage'); + await CallManager().initialize( + userId: chatLoginResponse.userId.toString(), + authToken: chatLoginResponse.token!, + conversationId: null, + moduleId: '1002', + referenceId: '0', + employeeNumber: myEmployeeNumber, + ); + + dev.log('✅ [LAND PAGE] CallManager initialized', name: 'LandPage'); + dev.log('═══════════════════════════════════════════', name: 'LandPage'); + dev.log('✅ [LAND PAGE] Calling system ready!', name: 'LandPage'); + dev.log(' ✅ SignalR: Connected', name: 'LandPage'); + dev.log(' ✅ CallManager: Ready', name: 'LandPage'); + dev.log(' ✅ WebRTC: Will initialize on first call', name: 'LandPage'); + dev.log(' ✅ Calls work from ANY screen now', name: 'LandPage'); + dev.log('═══════════════════════════════════════════', name: 'LandPage'); + + } catch (e, stackTrace) { + dev.log('❌ [LAND PAGE] Error initializing calling system: $e', + name: 'LandPage', error: e, stackTrace: stackTrace); + // Don't crash the app, just log the error + // User can still use the app, calls just won't work + } + } + + /// Cleanup SignalR and CallManager on logout + Future _cleanupCallingSystem() async { + try { + dev.log('🧹 [LAND PAGE] Cleaning up calling system...', name: 'LandPage'); + + // Reset CallManager + await CallManager().reset(); + + // Disconnect SignalR + await SignalRService().disconnect(); + + dev.log('✅ [LAND PAGE] Calling system cleaned up', name: 'LandPage'); + } catch (e) { + dev.log('⚠️ [LAND PAGE] Error during cleanup: $e', name: 'LandPage'); + } + } } diff --git a/lib/services/app_initialization_service.dart b/lib/services/app_initialization_service.dart new file mode 100644 index 00000000..77ae9172 --- /dev/null +++ b/lib/services/app_initialization_service.dart @@ -0,0 +1,111 @@ +import 'dart:developer'; +import 'package:flutter/foundation.dart'; +import 'package:test_sa/modules/cx_module/chat/services/call_manager.dart'; +import 'package:test_sa/modules/cx_module/chat/services/signalr_service.dart'; + +/// App-level initialization service +/// Handles initialization of core services like SignalR and CallManager +class AppInitializationService { + static final AppInitializationService _instance = AppInitializationService._internal(); + factory AppInitializationService() => _instance; + AppInitializationService._internal(); + + bool _isInitialized = false; + bool get isInitialized => _isInitialized; + + /// Initialize SignalR and CallManager after user login + /// This should be called from the landing page after successful login + Future initializeAfterLogin({ + required String userId, + required String authToken, + required String employeeNumber, + String? conversationId, + String? moduleId, + String? referenceId, + }) async { + if (_isInitialized) { + log('⚠️ [APP INIT] Already initialized, skipping...', name: 'AppInitializationService'); + return; + } + + try { + log('═══════════════════════════════════════════', name: 'AppInitializationService'); + log('🚀 [APP INIT] Starting app initialization...', name: 'AppInitializationService'); + log(' User ID: $userId', name: 'AppInitializationService'); + log(' Employee Number: $employeeNumber', name: 'AppInitializationService'); + log('═══════════════════════════════════════════', name: 'AppInitializationService'); + + // Step 1: Initialize SignalR Service + log('🔌 [APP INIT] Step 1: Initializing SignalR Service...', name: 'AppInitializationService'); + final signalRService = SignalRService(); + + final connected = await signalRService.initialize( + userId: userId, + authToken: authToken, + conversationId: conversationId, + ); + + if (!connected) { + throw Exception('Failed to initialize SignalR connection'); + } + log('✅ [APP INIT] SignalR Service initialized', name: 'AppInitializationService'); + log(' Connection State: ${signalRService.connectionState}', name: 'AppInitializationService'); + log(' Connection ID: ${signalRService.connectionId ?? "NULL"}', name: 'AppInitializationService'); + + // Step 2: Initialize CallManager + log('📞 [APP INIT] Step 2: Initializing CallManager...', name: 'AppInitializationService'); + await CallManager().initialize( + userId: userId, + authToken: authToken, + conversationId: conversationId, + moduleId: moduleId, + referenceId: referenceId, + employeeNumber: employeeNumber, + ); + log('✅ [APP INIT] CallManager initialized', name: 'AppInitializationService'); + + _isInitialized = true; + + log('═══════════════════════════════════════════', name: 'AppInitializationService'); + log('✅ [APP INIT] App initialization complete!', name: 'AppInitializationService'); + log(' ✅ SignalR Service: Ready', name: 'AppInitializationService'); + log(' ✅ CallManager: Ready', name: 'AppInitializationService'); + log(' ✅ WebRTC: Will initialize on first call', name: 'AppInitializationService'); + log('═══════════════════════════════════════════', name: 'AppInitializationService'); + + } catch (e, stackTrace) { + log('❌ [APP INIT] Initialization failed: $e', + name: 'AppInitializationService', error: e, stackTrace: stackTrace); + _isInitialized = false; + rethrow; + } + } + + /// Reset initialization state (for logout) + Future reset() async { + log('🔄 [APP INIT] Resetting app initialization...', name: 'AppInitializationService'); + + try { + // Reset CallManager + await CallManager().reset(); + + // Disconnect SignalR + await SignalRService().disconnect(); + + _isInitialized = false; + + log('✅ [APP INIT] Reset complete', name: 'AppInitializationService'); + } catch (e) { + log('⚠️ [APP INIT] Error during reset: $e', name: 'AppInitializationService'); + } + } + + /// Check if services are ready for calling + bool areServicesReady() { + final signalRConnected = SignalRService().isConnected; + final callManagerInitialized = CallManager().isCallInProgress || _isInitialized; + + return signalRConnected && _isInitialized; + } +} + diff --git a/pubspec.lock b/pubspec.lock index e9b535f0..62dcbadc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -686,6 +686,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.3" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" + url: "https://pub.dev" + source: hosted + version: "9.2.1" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b66eb3f9..e69de29b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,176 +0,0 @@ -name: test_sa -description: medical app to manage medical devices issues - -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.7.8+44 - -environment: - sdk: ">=3.5.0 <4.0.0" - -#localization_dir: assets\translations - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.5 - font_awesome_flutter: ^10.12.0 - http: ^1.6.0 - provider: ^6.1.5+1 - shared_preferences: ^2.5.4 - fluttertoast: ^9.0.0 - image_picker: ^1.2.1 - url_launcher: ^6.3.2 - flutter_launcher_icons: ^0.14.4 - package_info_plus: ^8.3.1 - share_plus: ^10.1.4 - flutter_local_notifications: ^17.2.4 - cached_network_image: ^3.4.1 - carousel_slider: ^5.1.2 - intl: ^0.20.2 - nfc_manager: ^4.2.1 - flutter_typeahead: ^5.2.0 - speech_to_text: ^7.3.0 - firebase_core: ^3.15.2 - firebase_messaging: ^15.2.10 - qr_code_scanner_plus: ^2.1.1 - flutter_sound: ^9.30.0 - permission_handler: ^11.4.0 - rive: ^0.14.4 - another_flushbar: ^1.12.32 - pinput: ^6.0.2 - audioplayers: ^6.6.0 - mobile_scanner: ^7.2.0 - - flare_flutter: - git: - url: https://github.com/mbfakourii/Flare-Flutter.git - path: flare_flutter - ref: remove_hashValues - - signature: ^5.5.0 - flutter_svg: ^2.2.3 - file_picker: ^10.3.10 - record_mp3_plus: ^1.4.1 - path_provider: ^2.1.5 - open_file: ^3.5.11 - localization: ^2.1.0 - dotted_border: ^2.1.0 - lottie: ^3.3.2 - shimmer: ^3.0.0 - date_picker_timeline: ^1.2.6 - flutter_advanced_switch: ^3.0.1 - table_calendar: ^3.0.8 - image_cropper: ^8.1.0 - flutter_timezone: ^3.0.1 - device_calendar: ^4.3.3 - pie_chart: ^5.4.0 - - badges: ^3.1.1 - # buttons_tabbar: ^1.1.2 - flutter_custom_month_picker: ^0.1.3 - local_auth: ^2.3.0 - google_api_availability: ^5.0.1 - huawei_push: ^6.14.0+300 - huawei_location: ^6.16.0+300 - geolocator: ^9.0.2 - wifi_iot: ^0.3.19+2 - just_audio: ^0.9.46 - safe_device: ^1.3.8 - toggle_switch: ^2.3.0 - tree_view_flutter: ^1.0.2 - audio_waveforms: ^1.3.0 - signalr_netcore: ^1.4.4 - ellipsized_text: ^2.0.0 - local_auth_darwin: ^1.4.0 - haptic_feedback: ^0.6.4+3 - sn_progress_dialog: ^1.2.0 - flutter_webrtc: ^1.5.2 - flutter_callkit_incoming: ^3.1.3 - uuid: ^4.5.3 - - -dev_dependencies: - flutter_test: - sdk: flutter -dependency_overrides: - sqflite_android: 2.4.1 - win32: ^5.5.4 - flutter_lints: ^2.0.0 - -flutter_icons: - ios: true - android: true - image_path_ios: "assets/images/app_logo.jpg" - image_path_android: "assets/images/app_logo.jpg" - -# The following section is specific to Flutter. -flutter: - config: - enable-swift-package-manager: false - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - generate: true - uses-material-design: true - assets: - - assets/ - - assets/audio/ - - assets/images/ - - assets/images/dashboard/ - - assets/images/wo/ - - assets/lottie/ - - assets/subtitles/ - - assets/rives/ - fonts: - - family: Swiss - fonts: - - asset: assets/fonts/Gotham_Rounded_Light.otf - weight: 300 - - asset: assets/fonts/Gotham_Rounded_Book.otf - weight: 400 - - asset: assets/fonts/Gotham_Rounded_Bold.otf - weight: 700 - - family: Poppins - fonts: - - asset: assets/fonts/poppins/Poppins-Black.ttf - weight: 900 - - asset: assets/fonts/poppins/Poppins-ExtraBold.ttf - weight: 800 - - asset: assets/fonts/poppins/Poppins-Bold.ttf - weight: 700 - - asset: assets/fonts/poppins/Poppins-SemiBold.ttf - weight: 600 - - asset: assets/fonts/poppins/Poppins-Medium.ttf - weight: 500 - - asset: assets/fonts/poppins/Poppins-Regular.ttf - weight: 400 - - asset: assets/fonts/poppins/Poppins-Light.ttf - weight: 300 - - asset: assets/fonts/poppins/Poppins-ExtraLight.ttf - weight: 200 - - asset: assets/fonts/poppins/Poppins-Thin.ttf - weight: 100