restuctured flow

ui_ux_rollout_merge_audio_video_call
WaseemAbbasi22 4 weeks ago
parent 518ab82814
commit c8674f6bf0

@ -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<void> setupServiceLocator() async {
// SignalR Service - ONLY ONE instance for the entire app
getIt.registerLazySingleton<SignalRService>(
() => SignalRService(),
);
print('✅ [DI] Service locator initialized');
print(' - SignalRService registered as singleton (hashCode: ${getIt<SignalRService>().hashCode})');
}
Future<void> resetServices() async {
await getIt<SignalRService>().reset();
print('✅ [DI] All services reset');
}

@ -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/cm_detail_provider.dart';
import 'package:test_sa/modules/cm_module/create_cm_request.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/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/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_period_lookup_provider.dart';
import 'package:test_sa/modules/demo_module/provider/demo_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 'providers/service_request_providers/reject_reason_provider.dart';
import 'modules/cleaner_module/pages/cleaner_search_form_view.dart'; import 'modules/cleaner_module/pages/cleaner_search_form_view.dart';
import 'modules/cleaner_module/provider/cleaner_provider.dart'; import 'modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/core/di/service_locator.dart';
// class MyHttpOverrides extends HttpOverrides { // class MyHttpOverrides extends HttpOverrides {
// @override // @override
@ -159,7 +161,7 @@ void main() async {
// HttpOverrides.global = MyHttpOverrides(); // for later use. // HttpOverrides.global = MyHttpOverrides(); // for later use.
_configureLocalTimeZone(); _configureLocalTimeZone();
NotificationManger.initialisation((notificationDetails) {}, (id, title, body, payload) async {}); NotificationManger.initialisation((notificationDetails) {}, (id, title, body, payload) async {});
await setupServiceLocator();
if (Platform.isIOS) { if (Platform.isIOS) {
await Firebase.initializeApp( await Firebase.initializeApp(
options: const FirebaseOptions( options: const FirebaseOptions(
@ -173,7 +175,6 @@ void main() async {
await Firebase.initializeApp(); await Firebase.initializeApp();
} }
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.white, systemNavigationBarColor: Colors.white,

@ -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/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_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/model/call_session.dart';
import 'package:test_sa/modules/cx_module/chat/call/video_call_page.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'; import 'package:test_sa/new_views/app_style/app_color.dart';
@ -21,35 +21,26 @@ class AudioCallPage extends StatefulWidget {
} }
class _AudioCallPageState extends State<AudioCallPage> { class _AudioCallPageState extends State<AudioCallPage> {
late CallManager _callManager;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_callManager = CallManager();
// Listen for call end and navigate back // Listen for call end and navigate back
WidgetsBinding.instance.addPostFrameCallback((_) { _callManager.addListener(_checkCallStatus);
final chatProvider = context.read<ChatProvider>();
// Add listener to detect when call ends
chatProvider.addListener(_checkCallStatus);
});
} }
void _checkCallStatus() { void _checkCallStatus() {
final chatProvider = context.read<ChatProvider>(); // If call status is idle (call ended), navigate back
// If call status is idle (call ended), navigate back to chat if (_callManager.callStatus == CallStatus.idle && mounted) {
if (chatProvider.callStatus == CallStatus.idle && mounted) {
// Remove listener before popping to avoid memory leaks
chatProvider.removeListener(_checkCallStatus);
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
@override @override
void dispose() { void dispose() {
// Clean up listener _callManager.removeListener(_checkCallStatus);
try {
context.read<ChatProvider>().removeListener(_checkCallStatus);
} catch (e) {
// Provider might already be disposed
}
super.dispose(); super.dispose();
} }
@ -62,9 +53,10 @@ class _AudioCallPageState extends State<AudioCallPage> {
arrowBackColor: AppColor.white10, arrowBackColor: AppColor.white10,
), ),
body: SafeArea( body: SafeArea(
child: Selector<ChatProvider, CallSession?>( child: ListenableBuilder(
selector: (_, provider) => provider.currentCall, listenable: _callManager,
builder: (context, session, _) { builder: (context, _) {
final session = _callManager.currentCall;
if (session == null) { if (session == null) {
return const Center(child: Text('No active call')); return const Center(child: Text('No active call'));
} }
@ -110,6 +102,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
} }
Widget _buildContactInfo(BuildContext context, CallSession session) { Widget _buildContactInfo(BuildContext context, CallSession session) {
final isPeerMuted = _callManager.isPeerMuted;
return Column( return Column(
children: [ children: [
Stack( Stack(
@ -120,7 +114,6 @@ class _AudioCallPageState extends State<AudioCallPage> {
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColor.whiteF8d, color: AppColor.whiteF8d,
//TODO need to check opacity
border: Border.all(color: AppColor.white10.withOpacity(0.2), width: 1), border: Border.all(color: AppColor.white10.withOpacity(0.2), width: 1),
), ),
child: ClipOval( child: ClipOval(
@ -139,12 +132,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
), ),
), ),
// Show mute indicator when peer is muted // Show mute indicator when peer is muted
Selector<ChatProvider, bool>( if (isPeerMuted)
selector: (_, provider) => provider.isPeerMuted, Positioned(
builder: (context, isPeerMuted, _) {
if (!isPeerMuted) return const SizedBox.shrink();
return Positioned(
bottom: 0, bottom: 0,
right: 0, right: 0,
child: Container( child: Container(
@ -160,8 +149,6 @@ class _AudioCallPageState extends State<AudioCallPage> {
color: AppColor.white10, color: AppColor.white10,
), ),
), ),
);
},
), ),
], ],
), ),
@ -175,12 +162,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
// Show "Microphone is off" text when peer is muted // Show "Microphone is off" text when peer is muted
Selector<ChatProvider, bool>( if (isPeerMuted)
selector: (_, provider) => provider.isPeerMuted, Column(
builder: (context, isPeerMuted, _) {
if (!isPeerMuted) return const SizedBox.shrink();
return Column(
children: [ children: [
8.height, 8.height,
Container( Container(
@ -209,140 +192,101 @@ class _AudioCallPageState extends State<AudioCallPage> {
), ),
), ),
], ],
);
},
), ),
], ],
); );
} }
Widget _buildCallStatusOrDuration(BuildContext context) { Widget _buildCallStatusOrDuration(BuildContext context) {
return Selector<ChatProvider, CallStatus>( final status = _callManager.callStatus;
selector: (_, provider) => provider.callStatus, final duration = _callManager.callDuration;
builder: (context, status, _) {
String statusText; String statusText;
switch (status) { if (status == CallStatus.connected) {
case CallStatus.outgoingRinging: // Show call duration
statusText = 'Calling...'; final minutes = duration.inMinutes;
break; final seconds = duration.inSeconds % 60;
case CallStatus.incomingRinging: statusText = '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
statusText = 'Incoming call...'; } else {
break; // Show call status
case CallStatus.connecting: statusText = _getStatusText(status);
statusText = 'Connecting...';
break;
case CallStatus.connected:
return _buildDuration(context);
default:
statusText = '';
} }
return Text( return Text(
statusText, statusText,
style: AppTextStyles.heading5.copyWith( style: AppTextStyles.bodyText.copyWith(color: Colors.white70),
color: AppColor.neutral100,
),
);
},
); );
} }
Widget _buildDuration(BuildContext context) { String _getStatusText(CallStatus status) {
return Selector<ChatProvider, Duration>( switch (status) {
selector: (_, provider) => provider.callDuration, case CallStatus.connecting:
builder: (context, duration, _) { return 'Connecting...';
if (duration.inSeconds == 0) { case CallStatus.incomingRinging:
return const SizedBox.shrink(); 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));
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withAlpha(51),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'$minutes:$seconds',
style: AppTextStyles.heading5.copyWith(
color: AppColor.neutral100,
fontWeight: FontWeight.w400,
),
),
);
},
);
} }
Widget _buildControls(BuildContext context, CallSession session) { Widget _buildControls(BuildContext context, CallSession session) {
final chatProvider = context.read<ChatProvider>(); final isMuted = _callManager.isMuted;
return Column( final isSpeakerOn = _callManager.isSpeakerOn;
children: [
Row( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Selector<ChatProvider, bool>( _buildControlButton(
selector: (_, provider) => provider.isMuted, icon: isMuted ? Icons.mic_off : Icons.mic,
builder: (context, isMuted, _) {
return _buildControlButton(
icon: isMuted ? 'mic_disable' : 'mic_enable',
label: isMuted ? 'Unmute' : 'Mute', label: isMuted ? 'Unmute' : 'Mute',
isActive: isMuted, onPressed: () => _callManager.toggleMute(),
onPressed: () { backgroundColor: isMuted ? Colors.red : Colors.white24,
chatProvider.toggleMute(); ),
}, _buildControlButton(
); icon: isSpeakerOn ? Icons.volume_up : Icons.volume_down,
}, label: isSpeakerOn ? 'Speaker' : 'Earpiece',
), onPressed: () => _callManager.toggleSpeaker(),
// _buildControlButton( backgroundColor: isSpeakerOn ? Colors.blue : Colors.white24,
// icon: 'video_call_icon', ),
// label: 'Video', _buildControlButton(
// isActive: false, icon: Icons.call_end,
// onPressed: () { label: 'End',
// // Switch to video call view onPressed: () => _callManager.hangUp(),
// Navigator.pushReplacement( backgroundColor: Colors.red,
// context, iconColor: Colors.white,
// MaterialPageRoute(
// builder: (context) => const VideoCallPage(),
// ),
// );
// },
// ),
Selector<ChatProvider, bool>(
selector: (_, provider) => provider.isSpeakerOn,
builder: (context, isSpeakerOn, _) {
return _buildControlButton(
icon: 'speaker_enable',
label: 'Speaker',
isActive: isSpeakerOn,
onPressed: () {
chatProvider.toggleSpeaker();
},
);
},
), ),
], ],
), ),
48.height,
_buildEndCallButton(context, chatProvider),
],
); );
} }
Widget _buildControlButton({ Widget _buildControlButton({
required String icon, required IconData icon,
required String label, required String label,
required bool isActive,
required VoidCallback onPressed, required VoidCallback onPressed,
Color? backgroundColor,
Color? iconColor,
}) { }) {
return Column( return Column(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(18), 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))), decoration: BoxDecoration(
child: icon.toSvgAsset(width: 24, height: 24, color: AppColor.white10), 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(() { ).onPress(() {
onPressed(); onPressed();
}), }),
@ -354,26 +298,4 @@ class _AudioCallPageState extends State<AudioCallPage> {
], ],
); );
} }
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
});
}
} }

@ -3,21 +3,12 @@ import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:flutter_callkit_incoming/entities/entities.dart';
import 'package:test_sa/modules/cx_module/chat/services/call_manager.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 { class CallNotificationService {
static final CallNotificationService _instance = CallNotificationService._internal(); static final CallNotificationService _instance = CallNotificationService._internal();
factory CallNotificationService() => _instance; factory CallNotificationService() => _instance;
CallNotificationService._internal(); CallNotificationService._internal();
// Track processed call IDs to prevent duplicates
final Set<String> _processedCallIds = {}; final Set<String> _processedCallIds = {};
// Store pending call data for restoration after app wake
Map<String, dynamic>? _pendingCallData; Map<String, dynamic>? _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<void> handleIncomingCallNotification({ Future<void> handleIncomingCallNotification({
required Map<String, dynamic> notificationData, required Map<String, dynamic> notificationData,
required String source, // 'push' or 'signalr' required String source, // 'push' or 'signalr'
@ -26,12 +17,9 @@ class CallNotificationService {
log('═══════════════════════════════════════════', name: 'CallNotificationService'); log('═══════════════════════════════════════════', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Notification received from: $source', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Notification received from: $source', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Raw data: $notificationData', 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? ?? final notificationType = notificationData['notificationType'] as String? ??
notificationData['type'] as String?; // Backend uses 'type' notificationData['type'] as String?; // Backend uses 'type'
final transactionType = notificationData['transactionType'] as String?; final transactionType = notificationData['transactionType'] as String?;
log('📞 [INCOMING CALL] Notification Type: $notificationType', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Notification Type: $notificationType', name: 'CallNotificationService');
log('📞 [INCOMING CALL] Transaction Type: $transactionType', name: 'CallNotificationService'); log('📞 [INCOMING CALL] Transaction Type: $transactionType', name: 'CallNotificationService');

File diff suppressed because it is too large Load Diff

@ -190,7 +190,8 @@ class _ChatPageState extends State<ChatPage> {
@override @override
void dispose() { void dispose() {
chatHubConnection?.stop(); final chatProvider = Provider.of<ChatProvider>(context, listen: false);
chatProvider.chatHubConnection?.stop();
playerController.dispose(); playerController.dispose();
recorderController.dispose(); recorderController.dispose();
super.dispose(); super.dispose();
@ -419,13 +420,13 @@ class _ChatPageState extends State<ChatPage> {
textInputAction: TextInputAction.none, textInputAction: TextInputAction.none,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
onTap: () { onTap: () {
chatHubConnection!.invoke("SendTypingAsync", args: [receiver]); chatProvider.chatHubConnection!.invoke("SendTypingAsync", args: [receiver]);
}, },
onTapOutside: (PointerDownEvent event) { onTapOutside: (PointerDownEvent event) {
chatHubConnection!.invoke("SendStopTypingAsync", args: [receiver]); chatProvider.chatHubConnection!.invoke("SendStopTypingAsync", args: [receiver]);
}, },
onChanged: (text) { onChanged: (text) {
chatHubConnection!.invoke("SendTypingAsync", args: [receiver]); chatProvider.chatHubConnection!.invoke("SendTypingAsync", args: [receiver]);
}, },
decoration: InputDecoration( decoration: InputDecoration(
enabledBorder: InputBorder.none, enabledBorder: InputBorder.none,

@ -44,9 +44,8 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'call/call_error_handler.dart'; import 'call/call_error_handler.dart';
import 'call/services/call_notification_service.dart'; import 'call/services/call_notification_service.dart';
import 'services/call_manager.dart'; import 'services/call_manager.dart';
import 'services/signalr_service.dart'; // ADD: Import SignalRService import 'services/signalr_service.dart';
import 'package:test_sa/core/di/service_locator.dart';
HubConnection? chatHubConnection;
class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin { class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
bool isTyping = false; bool isTyping = false;
@ -72,6 +71,21 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
late int moduleID; late int moduleID;
int? referenceID; int? referenceID;
// SignalR Service - Retrieved from GetIt (Dependency Injection)
// DO NOT instantiate directly - always use getIt<SignalRService>()
SignalRService get _signalRService => getIt<SignalRService>();
// 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 === // === CALL STATE - DELEGATED TO CallManager ===
// These getters delegate to CallManager for backwards compatibility // These getters delegate to CallManager for backwards compatibility
CallStatus get callStatus => CallManager().callStatus; CallStatus get callStatus => CallManager().callStatus;
@ -330,545 +344,87 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
/// Prevents connection leaks if initialization fails /// Prevents connection leaks if initialization fails
Future<void> buildHubConnection(String conversationID) async { Future<void> buildHubConnection(String conversationID) async {
try { try {
// Dispose existing connection if any
await _disposeConnection();
chatHubConnection = await getHubConnection();
await chatHubConnection!.start();
// NEW: Log connection details for diagnostics
log('═══════════════════════════════════════════', name: 'ChatProvider'); log('═══════════════════════════════════════════', name: 'ChatProvider');
log('🔌 SignalR Hub Connection: Started', name: 'ChatProvider'); log('🔌 [ChatProvider] buildHubConnection called', name: 'ChatProvider');
log('🔑 Connection ID: ${chatHubConnection!.connectionId ?? "NULL"}', name: 'ChatProvider');
log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider');
log('💬 Conversation ID: $conversationID', name: 'ChatProvider'); log('💬 Conversation ID: $conversationID', name: 'ChatProvider');
log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider');
log('═══════════════════════════════════════════', name: 'ChatProvider');
if (kDebugMode) { // Use SignalR service from DI
print("🔌 SignalR Hub Connection: Started"); final signalRService = _signalRService;
}
log('🔍 [ChatProvider] SignalR service instance: ${signalRService.hashCode}', name: 'ChatProvider');
log(' Is connected: ${signalRService.isConnected}', name: 'ChatProvider');
log(' Connection state: ${signalRService.connectionState}', name: 'ChatProvider');
await chatHubConnection!.invoke("JoinConversation", args: [conversationID]); // Initialize SignalR if not already connected
log('✅ Joined conversation: $conversationID', name: 'ChatProvider'); if (!signalRService.isConnected) {
log('🔌 [ChatProvider] Initializing SignalR connection...', name: 'ChatProvider');
// CRITICAL: Share this connection with SignalRService for CallManager final connected = await signalRService.initialize(
log('🔗 [SIGNALR] Sharing connection with CallManager...', name: 'ChatProvider');
SignalRService().useExistingConnection(
chatHubConnection!,
userId: chatLoginResponse!.userId.toString(), userId: chatLoginResponse!.userId.toString(),
authToken: chatLoginResponse!.token ?? '', authToken: chatLoginResponse!.token ?? '',
conversationId: conversationID, conversationId: conversationID,
); );
log('✅ [SIGNALR] Connection shared with CallManager', name: 'ChatProvider');
// Register ALL event handlers (chat + call) if (!connected) {
_registerAllEventHandlers(); throw Exception('Failed to initialize SignalR connection');
//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 log('✅ [ChatProvider] SignalR initialized', name: 'ChatProvider');
void _registerAllEventHandlers() { log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider');
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<HubConnection> 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: <int>[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 { } else {
// Production: still need to re-register handlers on reconnect log('✅ [ChatProvider] SignalR already connected', name: 'ChatProvider');
hub.onreconnected(({String? connectionId}) async { log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider');
_registerAllEventHandlers();
});
print('✅ SignalR hub connection created');
}
return hub; // Just join the conversation if needed
if (conversationID.isNotEmpty) {
await signalRService.invoke("JoinConversation", args: [conversationID]);
log('✅ [ChatProvider] Joined conversation: $conversationID', name: 'ChatProvider');
}
} }
void registerEvents() { // Use the connection from SignalRService
// chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus); chatHubConnection = signalRService.hubConnection;
// chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived);
// chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); // Log user details
// chatHubConnection.on("OnUserTypingAsync", onUserTyping); log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider');
chatHubConnection?.on("OnUserCountAsync", userCountAsync); log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider');
// chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); log('═══════════════════════════════════════════', name: 'ChatProvider');
// chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered);
// chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus);
// chatHubConnection.on("OnGetGroupUserStatusAsync", getGroupUserStatus);
// // Register chat event handlers
// {"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}]]} _registerChatEventHandlers();
} catch (e, stackTrace) {
log('❌ [ChatProvider] Error building SignalR connection: $e',
name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) { if (kDebugMode) {
// logger.i("All listeners registered"); print('⚠️ Error building SignalR connection: $e');
}
}
// Future<void> 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();
// }
// }
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<SingleUserChatModel> 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();
// }
void markRead(List<SingleUserChatModel> 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();
}
// }
}
}
} }
// Clean up on error
Future<bool> resetCount({ await _disposeConnection();
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; rethrow;
} }
} }
void updateUserChatHistoryStatusAsync(List data) { /// Register chat event handlers (separate from call handlers)
try { void _registerChatEventHandlers() {
chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); if (chatHubConnection == null) {
} catch (e) { log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider');
throw e;
}
}
void updateUserChatHistoryOnMsg(List data) {
try {
chatHubConnection!.invoke("UpdateUserChatHistoryStatusAsync", args: [data]);
} catch (e) {
throw e;
}
}
// List<SingleUserChatModel> getSingleUserChatModel(String str) => List<SingleUserChatModel>.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x)));
List<SingleUserChatModel> getSingleUserChatModel(String str) {
final dynamic decodedJson = json.decode(str);
// Check if the decoded JSON is already a List
if (decodedJson is List) {
return List<SingleUserChatModel>.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<String, dynamic>) {
return [SingleUserChatModel.fromJson(decodedJson)];
}
// Handle unexpected types
else {
throw const FormatException('Expected a JSON object or a list of JSON objects.');
}
}
// List<groupchathistory.GetGroupChatHistoryAsync> getGroupChatHistoryAsync(String str) =>
// List<groupchathistory.GetGroupChatHistoryAsync>.from(json.decode(str).map((x) => groupchathistory.GetGroupChatHistoryAsync.fromJson(x)));
//
Future<dynamic> uploadAttachments(String userId, File file, String fileSource) async {
dynamic result;
try {
Map<String, String> 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;
} else {
result = [];
}
} catch (e) {
throw e;
}
return result;
}
Future<List<UnReadMessage>> getUnReadMessages(String employeeId) async {
// employeeId = "FMEngineer";
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 [];
}
} catch (error) {
return [];
}
}
// void updateUserChatStatus(List<Object?>? 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<Object?>? args) {
//note: need to implement this function...
print(args);
}
Future<void> markMessageAsRead(int messageId) async {
final senderId = sender?.userId;
if (senderId == null) return;
chatHubConnection?.invoke(
"SendMessageReadAsync",
args: [messageId, senderId],
);
}
void onChatSeen(List<Object?>? 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<Object?>? 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();
}
// void updateChatHistoryWindow(List<Object?>? 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<Object?>? 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<Object?>? 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<ChatUser>? 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<void> OnTypingAsync(List<Object?>? 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();
});
}
Future<void> OnStopTypingAsync(List<Object?>? parameters) async {
if (timer?.isActive ?? false) {
timer!.cancel();
}
isTyping = false;
notifyListeners();
}
Future<void> onSeenUserChatAsync(List<Object?>? 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<String, dynamic>) {
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');
}
}
Future<void> onAckSeenAsync(List<Object?>? 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; return;
} }
log('last list item id ${chatResponseList.first.toJson()}');
chatResponseList.first.isSeen = true;
notifyListeners();
} catch (e, stackTrace) {
log('onAckSeenAsync error: $e');
log('StackTrace: $stackTrace');
}
}
Future<void> onSubmitChatAsync(List<Object?>? parameters) async {
if (kDebugMode) print("OnSubmitChatAsync:$parameters");
if (parameters == null || parameters.isEmpty) return; log('🔧 [EVENT HANDLERS] Registering chat event handlers...', name: 'ChatProvider');
try { // Register chat event handlers via SignalRService
for (dynamic msg in parameters) { _signalRService.on("ReceiveMessage", onMsgReceived1);
var data = getSingleUserChatModel(jsonEncode(msg)); _signalRService.on("OnMessageReceivedAsync", onMsgReceived);
if (kDebugMode) print('Parsed message: $data'); _signalRService.on("OnSubmitChatAsync", onSubmitChatAsync);
} _signalRService.on("OnTypingAsync", OnTypingAsync);
} catch (e) { _signalRService.on("OnStopTypingAsync", OnStopTypingAsync);
if (kDebugMode) print('Error in OnSubmitChatAsync: $e'); _signalRService.on("OnSeenChatUserAsync", onSeenUserChatAsync);
} _signalRService.on("OnAckSeenAsync", onAckSeenAsync);
} _signalRService.on("OnCallHistoryUpdated", _onCallHistoryUpdated);
Future<void> onMsgReceived1(List<Object?>? parameters) async { log('✅ [EVENT HANDLERS] Chat event handlers registered successfully', name: 'ChatProvider');
print("onMsgReceived1:$parameters");
}
Future<void> onMsgReceived(List<Object?>? parameters) async {
List<SingleUserChatModel> 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...
} }
// ==================== CALL INFRASTRUCTURE ==================== // ==================== 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); log('❌ [CALL HISTORY] Error reloading chat history: $e', name: 'ChatProvider', error: e, stackTrace: stackTrace);
} }
} }
// ==================== CHAT EVENT HANDLERS ====================
Future<void> onMsgReceived1(List<Object?>? parameters) async {
print("onMsgReceived1:$parameters");
}
Future<void> onMsgReceived(List<Object?>? parameters) async {
try {
if (parameters != null && parameters.isNotEmpty) {
var data = parameters[0] as Map<Object?, Object?>;
SingleUserChatModel chatResponse = SingleUserChatModel.fromJson(Map<String, dynamic>.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<void> onSubmitChatAsync(List<Object?>? parameters) async {
try {
if (parameters != null && parameters.isNotEmpty) {
var data = parameters[0] as Map<Object?, Object?>;
SingleUserChatModel chatResponse = SingleUserChatModel.fromJson(Map<String, dynamic>.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<void> OnTypingAsync(List<Object?>? parameters) async {
try {
isTyping = true;
notifyListeners();
if (kDebugMode) {
print('✍️ User is typing...');
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in OnTypingAsync: $e');
}
}
}
Future<void> OnStopTypingAsync(List<Object?>? parameters) async {
try {
isTyping = false;
notifyListeners();
if (kDebugMode) {
print('✅ User stopped typing');
}
} catch (e) {
if (kDebugMode) {
print('⚠️ Error in OnStopTypingAsync: $e');
}
}
}
Future<void> onSeenUserChatAsync(List<Object?>? 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<void> onAckSeenAsync(List<Object?>? 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<bool> 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<List<ChatAttachment>?> 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<List<UnReadMessage>> getUnReadMessages(String employeeId) async { // Fixed: accept employeeId and return List<UnReadMessage>
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 [];
}
}
} }

@ -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:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:test_sa/core/di/service_locator.dart';
/// CallManager - Singleton service that manages all call responsibilities /// CallManager - Singleton service that manages all call responsibilities
/// Works independently of ChatProvider /// Works independently of ChatProvider
/// Uses SignalRService for SignalR communication /// Uses SignalRService via Dependency Injection
class CallManager extends ChangeNotifier { class CallManager extends ChangeNotifier {
static final CallManager _instance = CallManager._internal(); static final CallManager _instance = CallManager._internal();
factory CallManager() => _instance; factory CallManager() => _instance;
CallManager._internal(); CallManager._internal() {
log('🏗️ [CallManager] Singleton instance created (hashCode: $hashCode)', name: 'CallManager');
}
// Services // Services - Retrieved from DI
final SignalRService _signalRService = SignalRService(); SignalRService get _signalRService => getIt<SignalRService>();
final CallKitService _callKitService = CallKitService(); final CallKitService _callKitService = CallKitService();
WebRTCService? _webrtcService; WebRTCService? _webrtcService;
@ -996,7 +999,17 @@ class CallManager extends ChangeNotifier {
log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager'); log(' Current call ID: ${_currentCall?.callId}', name: 'CallManager');
log('═══════════════════════════════════════════', 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(); _cleanup();
log('✅ [HANGUP] Call ended and screen closed', name: 'CallManager');
log('═══════════════════════════════════════════', name: 'CallManager');
} }
void _handleOffer(List<Object?>? args) async { void _handleOffer(List<Object?>? args) async {

@ -5,28 +5,37 @@ import 'package:signalr_netcore/hub_connection.dart';
import 'package:signalr_netcore/signalr_client.dart'; import 'package:signalr_netcore/signalr_client.dart';
import 'package:test_sa/controllers/api_routes/urls.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 { class SignalRService {
// Singleton instance - DO NOT instantiate directly, use DI (Provider)
static final SignalRService _instance = SignalRService._internal(); static final SignalRService _instance = SignalRService._internal();
factory SignalRService() => _instance; 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; HubConnection? _hubConnection;
// Connection state // Connection state management
bool _isInitializing = false; bool _isInitializing = false;
Completer<bool>? _initializationCompleter;
// Authentication data // Authentication data
String? _userId; String? _userId;
String? _authToken; String? _authToken;
String? _currentConversationId; String? _currentConversationId;
// Event handler registrations // Event handler registrations for re-registration after reconnect
final Map<String, List<Function(List<Object?>?)>> _eventHandlers = {}; final Map<String, List<Function(List<Object?>?)>> _eventHandlers = {};
/// Get the current HubConnection // Connection state stream for reactive updates
final StreamController<HubConnectionState> _connectionStateController =
StreamController<HubConnectionState>.broadcast();
/// Stream of connection state changes for consumers to listen
Stream<HubConnectionState> get connectionStateStream => _connectionStateController.stream;
/// Get the current HubConnection (read-only access)
HubConnection? get hubConnection => _hubConnection; HubConnection? get hubConnection => _hubConnection;
/// Check if connected /// Check if connected
@ -35,66 +44,47 @@ class SignalRService {
/// Get current connection state /// Get current connection state
HubConnectionState? get connectionState => _hubConnection?.state; HubConnectionState? get connectionState => _hubConnection?.state;
/// Use an existing HubConnection (from ChatProvider) /// Get connection ID (for debugging and verification)
/// This prevents creating duplicate connections String? get connectionId => _hubConnection?.connectionId;
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();
}
/// Initialize SignalR connection with authentication /// Initialize SignalR connection with authentication
/// Thread-safe: Multiple callers will wait for the same connection task
Future<bool> initialize({ Future<bool> initialize({
required String userId, required String userId,
required String authToken, required String authToken,
String? conversationId, String? conversationId,
}) async { }) async {
try {
log('═══════════════════════════════════════════', name: 'SignalRService'); log('═══════════════════════════════════════════', name: 'SignalRService');
log('🔌 [SIGNALR] Initializing SignalR connection...', name: 'SignalRService'); log('🔌 [SIGNALR] initialize() called', name: 'SignalRService');
log(' Instance hashCode: ${hashCode}', name: 'SignalRService');
log(' User ID: $userId', name: 'SignalRService'); log(' User ID: $userId', name: 'SignalRService');
log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService'); log(' Conversation ID: ${conversationId ?? "none"}', name: 'SignalRService');
// Prevent multiple simultaneous initializations // THREAD SAFETY: If already initializing, wait for current initialization
if (_isInitializing) { if (_isInitializing && _initializationCompleter != null) {
log('⏳ [SIGNALR] Already initializing, waiting...', name: 'SignalRService'); log('⏳ [SIGNALR] Already initializing, waiting for completion...', name: 'SignalRService');
// Wait for current initialization to complete return await _initializationCompleter!.future;
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 already connected with same credentials, reuse connection
if (isConnected && _userId == userId && _authToken == authToken) { if (isConnected && _userId == userId && _authToken == authToken) {
log('✅ [SIGNALR] Already connected with same credentials, reusing connection', name: 'SignalRService'); log('✅ [SIGNALR] Already connected with same credentials', name: 'SignalRService');
log(' Connection ID: ${_hubConnection?.connectionId}', name: 'SignalRService');
// Join new conversation if provided and different // Join new conversation if provided and different
if (conversationId != null && conversationId != _currentConversationId) { if (conversationId != null && conversationId != _currentConversationId) {
await _joinConversation(conversationId); await _joinConversation(conversationId);
} }
log('═══════════════════════════════════════════', name: 'SignalRService');
return true; return true;
} }
// Start new initialization
_isInitializing = true; _isInitializing = true;
_initializationCompleter = Completer<bool>();
try {
// Store credentials // Store credentials
_userId = userId; _userId = userId;
_authToken = authToken; _authToken = authToken;
@ -104,6 +94,8 @@ class SignalRService {
await _disposeConnection(); await _disposeConnection();
// Create new connection // Create new connection
log('🔧 [SIGNALR] Creating new HubConnection...', name: 'SignalRService');
final httpOp = HttpConnectionOptions( final httpOp = HttpConnectionOptions(
skipNegotiation: false, skipNegotiation: false,
logMessageContent: kDebugMode, logMessageContent: kDebugMode,
@ -117,15 +109,21 @@ class SignalRService {
.withAutomaticReconnect(retryDelays: <int>[2000, 5000, 10000, 20000]) .withAutomaticReconnect(retryDelays: <int>[2000, 5000, 10000, 20000])
.build(); .build();
// Setup reconnection handlers log('✅ [SIGNALR] HubConnection created (hashCode: ${_hubConnection.hashCode})', name: 'SignalRService');
// Setup reconnection handlers BEFORE starting connection
_setupReconnectionHandlers(); _setupReconnectionHandlers();
// Start connection // Start connection
log('🔌 [SIGNALR] Starting connection...', name: 'SignalRService');
await _hubConnection!.start(); await _hubConnection!.start();
log('✅ [SIGNALR] Connection established', name: 'SignalRService'); log('✅ [SIGNALR] Connection established', name: 'SignalRService');
log(' Connection ID: ${_hubConnection!.connectionId}', name: 'SignalRService'); log(' Connection ID: ${_hubConnection!.connectionId}', name: 'SignalRService');
// Emit connected state
_connectionStateController.add(HubConnectionState.Connected);
// Join conversation if provided // Join conversation if provided
if (conversationId != null) { if (conversationId != null) {
await _joinConversation(conversationId); await _joinConversation(conversationId);
@ -135,6 +133,7 @@ class SignalRService {
_reregisterAllHandlers(); _reregisterAllHandlers();
_isInitializing = false; _isInitializing = false;
_initializationCompleter?.complete(true);
log('═══════════════════════════════════════════', name: 'SignalRService'); log('═══════════════════════════════════════════', name: 'SignalRService');
return true; return true;
@ -142,7 +141,11 @@ class SignalRService {
} catch (e, stackTrace) { } catch (e, stackTrace) {
log('❌ [SIGNALR] Error initializing connection: $e', log('❌ [SIGNALR] Error initializing connection: $e',
name: 'SignalRService', error: e, stackTrace: stackTrace); name: 'SignalRService', error: e, stackTrace: stackTrace);
_isInitializing = false; _isInitializing = false;
_initializationCompleter?.complete(false);
log('═══════════════════════════════════════════', name: 'SignalRService');
return false; return false;
} }
} }
@ -153,14 +156,17 @@ class SignalRService {
_hubConnection!.onclose(({Exception? error}) { _hubConnection!.onclose(({Exception? error}) {
log('🔴 [SIGNALR] Connection closed: $error', name: 'SignalRService'); log('🔴 [SIGNALR] Connection closed: $error', name: 'SignalRService');
_connectionStateController.add(HubConnectionState.Disconnected);
}); });
_hubConnection!.onreconnecting(({Exception? error}) { _hubConnection!.onreconnecting(({Exception? error}) {
log('🟡 [SIGNALR] Reconnecting: $error', name: 'SignalRService'); log('🟡 [SIGNALR] Reconnecting: $error', name: 'SignalRService');
_connectionStateController.add(HubConnectionState.Reconnecting);
}); });
_hubConnection!.onreconnected(({String? connectionId}) async { _hubConnection!.onreconnected(({String? connectionId}) async {
log('🟢 [SIGNALR] Reconnected: $connectionId', name: 'SignalRService'); log('🟢 [SIGNALR] Reconnected: $connectionId', name: 'SignalRService');
_connectionStateController.add(HubConnectionState.Connected);
// Rejoin conversation if we had one // Rejoin conversation if we had one
if (_currentConversationId != null) { if (_currentConversationId != null) {
@ -169,6 +175,8 @@ class SignalRService {
// Re-register all event handlers // Re-register all event handlers
_reregisterAllHandlers(); _reregisterAllHandlers();
log('✅ [SIGNALR] Reconnection complete', name: 'SignalRService');
}); });
} }
@ -189,6 +197,7 @@ class SignalRService {
} }
/// Register an event handler /// Register an event handler
/// Events are stored and automatically re-registered after reconnect
void on(String eventName, Function(List<Object?>?) handler) { void on(String eventName, Function(List<Object?>?) handler) {
log('🔧 [SIGNALR] Registering handler for: $eventName', name: 'SignalRService'); log('🔧 [SIGNALR] Registering handler for: $eventName', name: 'SignalRService');
@ -196,11 +205,21 @@ class SignalRService {
if (!_eventHandlers.containsKey(eventName)) { if (!_eventHandlers.containsKey(eventName)) {
_eventHandlers[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); _eventHandlers[eventName]!.add(handler);
// Register with SignalR if connected // Register with SignalR if connected
if (_hubConnection != null) { if (_hubConnection != null) {
_hubConnection!.on(eventName, handler); _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() { void _reregisterAllHandlers() {
if (_hubConnection == null) return; 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) { for (final entry in _eventHandlers.entries) {
final eventName = entry.key; final eventName = entry.key;
final handlers = entry.value; final handlers = entry.value;
for (final handler in handlers) { for (final handler in handlers) {
_hubConnection!.on(eventName, handler); _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 /// Invoke a SignalR method
@ -248,16 +269,22 @@ class SignalRService {
} }
log('📤 [SIGNALR] Invoking: $methodName', name: '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); return await _hubConnection!.invoke(methodName, args: args);
} }
/// Ensure connection is ready (connect if needed) /// Ensure connection is ready (connect if needed)
/// Thread-safe: Multiple callers will wait for the same connection task
Future<bool> ensureConnected() async { Future<bool> ensureConnected() async {
if (isConnected) { if (isConnected) {
return true; return true;
} }
if (_userId != null && _authToken != null) { if (_userId != null && _authToken != null) {
log('🔄 [SIGNALR] Not connected, attempting to reconnect...', name: 'SignalRService');
return await initialize( return await initialize(
userId: _userId!, userId: _userId!,
authToken: _authToken!, authToken: _authToken!,
@ -273,6 +300,7 @@ class SignalRService {
Future<void> _disposeConnection() async { Future<void> _disposeConnection() async {
try { try {
if (_hubConnection != null) { if (_hubConnection != null) {
log('🔌 [SIGNALR] Disposing existing connection...', name: 'SignalRService');
await _hubConnection!.stop(); await _hubConnection!.stop();
_hubConnection = null; _hubConnection = null;
log('✅ [SIGNALR] Connection disposed', name: 'SignalRService'); log('✅ [SIGNALR] Connection disposed', name: 'SignalRService');
@ -284,6 +312,7 @@ class SignalRService {
/// Reset service (for logout) /// Reset service (for logout)
Future<void> reset() async { Future<void> reset() async {
log('═══════════════════════════════════════════', name: 'SignalRService');
log('🔄 [SIGNALR] Resetting service...', name: 'SignalRService'); log('🔄 [SIGNALR] Resetting service...', name: 'SignalRService');
await _disposeConnection(); await _disposeConnection();
@ -293,7 +322,19 @@ class SignalRService {
_authToken = null; _authToken = null;
_currentConversationId = null; _currentConversationId = null;
_isInitializing = false; _isInitializing = false;
_initializationCompleter = null;
log('✅ [SIGNALR] Service reset complete', name: 'SignalRService'); log('✅ [SIGNALR] Service reset complete', name: 'SignalRService');
log('═══════════════════════════════════════════', name: 'SignalRService');
}
/// Disconnect from SignalR (alias for reset)
Future<void> disconnect() async {
await reset();
}
/// Dispose stream controller (called when app is terminating)
void dispose() {
_connectionStateController.close();
} }
} }

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer' as dev;
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; 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/new_views/pages/settings_page.dart';
import 'package:test_sa/providers/work_order/vendor_provider.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/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 '../../../controllers/providers/settings/setting_provider.dart';
import '../../../views/widgets/dialogs/dialog.dart'; import '../../../views/widgets/dialogs/dialog.dart';
@ -124,19 +128,22 @@ class _LandPageState extends State<LandPage> {
DashboardView(onDrawerPress: (() { DashboardView(onDrawerPress: (() {
_scaffoldKey.currentState!.isDrawerOpen ? _scaffoldKey.currentState!.closeDrawer() : _scaffoldKey.currentState!.openDrawer(); _scaffoldKey.currentState!.isDrawerOpen ? _scaffoldKey.currentState!.closeDrawer() : _scaffoldKey.currentState!.openDrawer();
})), })),
// const old_page.LandPage(),
const MyRequestsPage(), const MyRequestsPage(),
// if (_userProvider!.user!.type != UsersTypes.engineer)
const SizedBox(), const SizedBox(),
// if (_userProvider!.user!.type != UsersTypes.engineer) const CalendarPage(),
const MyAssetsPage(fromBottomBar: true), const MyAssetsPage(fromBottomBar: true),
]; ];
if (isFCM) { if (isFCM) {
FirebaseNotificationManger.initialized(context); FirebaseNotificationManger.initialized(context);
NotificationManger.initialisation((notificationDetails) { NotificationManger.initialisation((notificationDetails) {
FirebaseNotificationManger.handleMessage(context, json.decode(notificationDetails.payload!)); FirebaseNotificationManger.handleMessage(context, json.decode(notificationDetails.payload!));
}, (id, title, body, payload) async {}); }, (id, title, body, payload) async {});
// Initialize SignalR & CallManager ONCE at app startup
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _initializeCallingSystem();
});
isFCM = false; isFCM = false;
} }
checkLocalAuth(); checkLocalAuth();
@ -149,6 +156,9 @@ class _LandPageState extends State<LandPage> {
builder: (_) => AAlertDialog(title: context.translation.signOut, content: context.translation.logoutAlert), builder: (_) => AAlertDialog(title: context.translation.signOut, content: context.translation.logoutAlert),
); );
if (result) { if (result) {
// Cleanup calling system before logout
await _cleanupCallingSystem();
bool isSuccess = await Provider.of<UserProvider>(context, listen: false).logout(context); bool isSuccess = await Provider.of<UserProvider>(context, listen: false).logout(context);
if (isSuccess) { if (isSuccess) {
Provider.of<SettingProvider>(context, listen: false).resetSettings(); Provider.of<SettingProvider>(context, listen: false).resetSettings();
@ -209,4 +219,107 @@ class _LandPageState extends State<LandPage> {
), ),
); );
} }
/// Initialize SignalR and CallManager once at app startup
/// This ensures calls work from ANY screen without CMDetailPage dependency
Future<void> _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<void> _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');
}
}
} }

@ -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<void> 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<void> 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;
}
}

@ -686,6 +686,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.3" 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: glob:
dependency: transitive dependency: transitive
description: description:

@ -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
Loading…
Cancel
Save