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/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,

@ -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<AudioCallPage> {
late CallManager _callManager;
@override
void initState() {
super.initState();
_callManager = CallManager();
// Listen for call end and navigate back
WidgetsBinding.instance.addPostFrameCallback((_) {
final chatProvider = context.read<ChatProvider>();
// Add listener to detect when call ends
chatProvider.addListener(_checkCallStatus);
});
_callManager.addListener(_checkCallStatus);
}
void _checkCallStatus() {
final chatProvider = context.read<ChatProvider>();
// 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<ChatProvider>().removeListener(_checkCallStatus);
} catch (e) {
// Provider might already be disposed
}
_callManager.removeListener(_checkCallStatus);
super.dispose();
}
@ -62,9 +53,10 @@ class _AudioCallPageState extends State<AudioCallPage> {
arrowBackColor: AppColor.white10,
),
body: SafeArea(
child: Selector<ChatProvider, CallSession?>(
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<AudioCallPage> {
}
Widget _buildContactInfo(BuildContext context, CallSession session) {
final isPeerMuted = _callManager.isPeerMuted;
return Column(
children: [
Stack(
@ -120,7 +114,6 @@ class _AudioCallPageState extends State<AudioCallPage> {
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,12 +132,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
),
),
// Show mute indicator when peer is muted
Selector<ChatProvider, bool>(
selector: (_, provider) => provider.isPeerMuted,
builder: (context, isPeerMuted, _) {
if (!isPeerMuted) return const SizedBox.shrink();
return Positioned(
if (isPeerMuted)
Positioned(
bottom: 0,
right: 0,
child: Container(
@ -160,8 +149,6 @@ class _AudioCallPageState extends State<AudioCallPage> {
color: AppColor.white10,
),
),
);
},
),
],
),
@ -175,12 +162,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
textAlign: TextAlign.center,
),
// Show "Microphone is off" text when peer is muted
Selector<ChatProvider, bool>(
selector: (_, provider) => provider.isPeerMuted,
builder: (context, isPeerMuted, _) {
if (!isPeerMuted) return const SizedBox.shrink();
return Column(
if (isPeerMuted)
Column(
children: [
8.height,
Container(
@ -209,140 +192,101 @@ class _AudioCallPageState extends State<AudioCallPage> {
),
),
],
);
},
),
],
);
}
Widget _buildCallStatusOrDuration(BuildContext context) {
return Selector<ChatProvider, CallStatus>(
selector: (_, provider) => provider.callStatus,
builder: (context, status, _) {
final status = _callManager.callStatus;
final duration = _callManager.callDuration;
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 = '';
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.heading5.copyWith(
color: AppColor.neutral100,
),
);
},
style: AppTextStyles.bodyText.copyWith(color: Colors.white70),
);
}
Widget _buildDuration(BuildContext context) {
return Selector<ChatProvider, Duration>(
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));
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) {
final chatProvider = context.read<ChatProvider>();
return Column(
children: [
Row(
final isMuted = _callManager.isMuted;
final isSpeakerOn = _callManager.isSpeakerOn;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Selector<ChatProvider, bool>(
selector: (_, provider) => provider.isMuted,
builder: (context, isMuted, _) {
return _buildControlButton(
icon: isMuted ? 'mic_disable' : 'mic_enable',
_buildControlButton(
icon: isMuted ? Icons.mic_off : Icons.mic,
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<ChatProvider, bool>(
selector: (_, provider) => provider.isSpeakerOn,
builder: (context, isSpeakerOn, _) {
return _buildControlButton(
icon: 'speaker_enable',
label: 'Speaker',
isActive: isSpeakerOn,
onPressed: () {
chatProvider.toggleSpeaker();
},
);
},
onPressed: () => _callManager.toggleMute(),
backgroundColor: isMuted ? Colors.red : Colors.white24,
),
_buildControlButton(
icon: isSpeakerOn ? Icons.volume_up : Icons.volume_down,
label: isSpeakerOn ? 'Speaker' : 'Earpiece',
onPressed: () => _callManager.toggleSpeaker(),
backgroundColor: isSpeakerOn ? Colors.blue : Colors.white24,
),
_buildControlButton(
icon: Icons.call_end,
label: 'End',
onPressed: () => _callManager.hangUp(),
backgroundColor: Colors.red,
iconColor: Colors.white,
),
],
),
48.height,
_buildEndCallButton(context, chatProvider),
],
);
}
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<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: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<String> _processedCallIds = {};
// Store pending call data for restoration after app wake
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({
required Map<String, dynamic> 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');

File diff suppressed because it is too large Load Diff

@ -190,7 +190,8 @@ class _ChatPageState extends State<ChatPage> {
@override
void dispose() {
chatHubConnection?.stop();
final chatProvider = Provider.of<ChatProvider>(context, listen: false);
chatProvider.chatHubConnection?.stop();
playerController.dispose();
recorderController.dispose();
super.dispose();
@ -419,13 +420,13 @@ class _ChatPageState extends State<ChatPage> {
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,

@ -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>()
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 ===
// 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<void> 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");
}
// Use SignalR service from DI
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]);
log('✅ Joined conversation: $conversationID', name: 'ChatProvider');
// Initialize SignalR if not already connected
if (!signalRService.isConnected) {
log('🔌 [ChatProvider] Initializing SignalR connection...', name: 'ChatProvider');
// CRITICAL: Share this connection with SignalRService for CallManager
log('🔗 [SIGNALR] Sharing connection with CallManager...', name: 'ChatProvider');
SignalRService().useExistingConnection(
chatHubConnection!,
final connected = await signalRService.initialize(
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
}
if (!connected) {
throw Exception('Failed to initialize SignalR connection');
}
/// 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<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');
log('✅ [ChatProvider] SignalR initialized', name: 'ChatProvider');
log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider');
} else {
// Production: still need to re-register handlers on reconnect
hub.onreconnected(({String? connectionId}) async {
_registerAllEventHandlers();
});
print('✅ SignalR hub connection created');
}
log('✅ [ChatProvider] SignalR already connected', name: 'ChatProvider');
log(' Connection ID: ${signalRService.connectionId}', name: 'ChatProvider');
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() {
// chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus);
// chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived);
// Use the connection from SignalRService
chatHubConnection = signalRService.hubConnection;
// 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);
// Log user details
log('👤 User ID: ${chatLoginResponse!.userId}', name: 'ChatProvider');
log('📞 My Employee Number: ${sender?.employeeNumber ?? "NOT SET"}', name: 'ChatProvider');
log('═══════════════════════════════════════════', name: 'ChatProvider');
//
// {"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}]]}
// Register chat event handlers
_registerChatEventHandlers();
} catch (e, stackTrace) {
log('❌ [ChatProvider] Error building SignalR connection: $e',
name: 'ChatProvider', error: e, stackTrace: stackTrace);
if (kDebugMode) {
// logger.i("All listeners registered");
}
}
// 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();
}
// }
}
}
print('⚠️ Error building SignalR connection: $e');
}
Future<bool> 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');
// Clean up on error
await _disposeConnection();
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<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');
/// Register chat event handlers (separate from call handlers)
void _registerChatEventHandlers() {
if (chatHubConnection == null) {
log('⚠️ Cannot register event handlers - connection is null', name: 'ChatProvider');
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 {
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');
}
}
// 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<void> onMsgReceived1(List<Object?>? parameters) async {
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...
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<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: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<SignalRService>();
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<Object?>? args) async {

@ -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<bool>? _initializationCompleter;
// Authentication data
String? _userId;
String? _authToken;
String? _currentConversationId;
// Event handler registrations
// Event handler registrations for re-registration after reconnect
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;
/// 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<bool> initialize({
required String userId,
required String authToken,
String? conversationId,
}) async {
try {
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(' 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;
// 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;
}
// 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('✅ [SIGNALR] Already connected with same credentials', name: 'SignalRService');
log(' Connection ID: ${_hubConnection?.connectionId}', name: 'SignalRService');
// Join new conversation if provided and different
if (conversationId != null && conversationId != _currentConversationId) {
await _joinConversation(conversationId);
}
log('═══════════════════════════════════════════', name: 'SignalRService');
return true;
}
// Start new initialization
_isInitializing = true;
_initializationCompleter = Completer<bool>();
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: <int>[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<Object?>?) 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<bool> 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<void> _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<void> 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<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: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<LandPage> {
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<LandPage> {
builder: (_) => AAlertDialog(title: context.translation.signOut, content: context.translation.logoutAlert),
);
if (result) {
// Cleanup calling system before logout
await _cleanupCallingSystem();
bool isSuccess = await Provider.of<UserProvider>(context, listen: false).logout(context);
if (isSuccess) {
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"
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:

@ -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