Compare commits

..

No commits in common. 'b212be19c6ee6d8fb5ec75fb7e90cda90fda3263' and '012314616ab705f7617a11d6a540d5eb4b4b8510' have entirely different histories.

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- You can change the hex code (#FFFFFF) to any color you want for your icon background -->
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

@ -4,18 +4,17 @@ class URLs {
static const String appReleaseBuildNumber = "44"; static const String appReleaseBuildNumber = "44";
// static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsm.hmg.com"; // production url
static const host1 = "https://atomsmdev.hmg.com"; // local DEV url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
// static const host1 = "https://atomsmuat.hmg.com"; // local UAT url static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/mobile"; // host local UAT
// static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
// static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM
// static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module
// static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation
// static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module
// static const String chatHubUrl = "https://apiderichat.hmg.com/chathub"; static const String chatHubUrl = "https://apiderichat.hmg.com/chathub";
static const String chatHubUrl = "$host1/api/DeriChat";
static const String chatHubUrlApi = "$chatHubUrl/api"; // new V2 apis static const String chatHubUrlApi = "$chatHubUrl/api"; // new V2 apis
static const String chatHubUrlChat = "$chatHubUrl/hubs/chat"; // new V2 apis static const String chatHubUrlChat = "$chatHubUrl/hubs/chat"; // new V2 apis

@ -26,7 +26,8 @@ import 'package:flutter_callkit_incoming/entities/entities.dart';
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async { Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
try { try {
log('📱 [BACKGROUND] Data payload: ${message.data}', name: 'FirebaseNotificationManger'); log('📱 [BACKGROUND] Data payload: ${message.data}', name: 'FirebaseNotificationManger');
final notificationType = message.data['notificationType'] as String? ?? message.data['type'] as String?; final notificationType = message.data['notificationType'] as String? ??
message.data['type'] as String?;
final transactionType = message.data['transactionType'] as String?; final transactionType = message.data['transactionType'] as String?;
log('📱 [BACKGROUND] Notification Type: $notificationType', name: 'FirebaseNotificationManger'); log('📱 [BACKGROUND] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
@ -34,11 +35,19 @@ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Handle incoming call notifications // Handle incoming call notifications
if (notificationType == 'incoming_call' || transactionType == 'call') { if (notificationType == 'incoming_call' || transactionType == 'call') {
// Extract call data // Extract call data
final callId = message.data['callId'] as String? ?? message.data['sessionId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(); final callId = message.data['callId'] as String? ??
message.data['sessionId'] as String? ??
DateTime.now().millisecondsSinceEpoch.toString();
final callerId = message.data['callerId'] as String? ?? message.data['callerEmployeeNumber'] as String? ?? message.data['sourceUserId'] as String? ?? 'unknown'; final callerId = message.data['callerId'] as String? ??
message.data['callerEmployeeNumber'] as String? ??
message.data['sourceUserId'] as String? ??
'unknown';
final callerName = message.data['callerName'] as String? ?? message.data['callerUserName'] as String? ?? message.data['userName'] as String? ?? 'Unknown Caller'; final callerName = message.data['callerName'] as String? ??
message.data['callerUserName'] as String? ??
message.data['userName'] as String? ??
'Unknown Caller';
final callerEmployeeNumber = message.data['callerEmployeeNumber'] as String? ?? callerId; final callerEmployeeNumber = message.data['callerEmployeeNumber'] as String? ?? callerId;
@ -50,7 +59,8 @@ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
return false; return false;
}(); }();
final moduleId = message.data['moduleId'] as String? ?? message.data['applicationId']?.toString(); final moduleId = message.data['moduleId'] as String? ??
message.data['applicationId']?.toString();
final referenceId = message.data['referenceId'] as String?; final referenceId = message.data['referenceId'] as String?;
final conversationId = message.data['conversationId'] as String?; final conversationId = message.data['conversationId'] as String?;
@ -131,7 +141,8 @@ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
} catch (e, stackTrace) { } catch (e, stackTrace) {
log('❌ [BACKGROUND] Error handling background message: $e', name: 'FirebaseNotificationManger', error: e, stackTrace: stackTrace); log('❌ [BACKGROUND] Error handling background message: $e',
name: 'FirebaseNotificationManger', error: e, stackTrace: stackTrace);
} }
} }
@ -177,7 +188,8 @@ class FirebaseNotificationManger {
//print("pushToken:$token"); //print("pushToken:$token");
} }
static void _onMessageReceived(h_push.RemoteMessage remoteMessage) {} static void _onMessageReceived(h_push.RemoteMessage remoteMessage) {
}
static void _onMessageReceiveError(Object error) { static void _onMessageReceiveError(Object error) {
log('❌ [HUAWEI] Message receive error: ${error.toString()}', name: 'FirebaseNotificationManger'); log('❌ [HUAWEI] Message receive error: ${error.toString()}', name: 'FirebaseNotificationManger');
@ -202,18 +214,27 @@ class FirebaseNotificationManger {
log('📱 [HANDLE_MESSAGE] Full message data: $messageData', name: 'FirebaseNotificationManger'); log('📱 [HANDLE_MESSAGE] Full message data: $messageData', name: 'FirebaseNotificationManger');
// FIXED: Check if this is a call notification first - CHECK BOTH 'notificationType' AND 'type' // FIXED: Check if this is a call notification first - CHECK BOTH 'notificationType' AND 'type'
final notificationType = messageData['notificationType'] as String? ?? messageData['type'] as String?; // Backend uses 'type' final notificationType = messageData['notificationType'] as String? ??
messageData['type'] as String?; // Backend uses 'type'
final transactionType = messageData['transactionType'] as String?; final transactionType = messageData['transactionType'] as String?;
log('📱 [HANDLE_MESSAGE] Notification Type: $notificationType', name: 'FirebaseNotificationManger'); log('📱 [HANDLE_MESSAGE] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
if (notificationType == 'incoming_call' || transactionType == 'call') { if (notificationType == 'incoming_call' || transactionType == 'call') {
// CRITICAL FIX: Show CallKit UI in foreground just like background // CRITICAL FIX: Show CallKit UI in foreground just like background
final callId = messageData['callId'] as String? ?? messageData['sessionId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(); final callId = messageData['callId'] as String? ??
messageData['sessionId'] as String? ??
DateTime.now().millisecondsSinceEpoch.toString();
final callerId = messageData['callerId'] as String? ?? messageData['callerEmployeeNumber'] as String? ?? messageData['sourceUserId'] as String? ?? 'unknown'; final callerId = messageData['callerId'] as String? ??
messageData['callerEmployeeNumber'] as String? ??
messageData['sourceUserId'] as String? ??
'unknown';
final callerName = messageData['callerName'] as String? ?? messageData['callerUserName'] as String? ?? messageData['userName'] as String? ?? 'Unknown Caller'; final callerName = messageData['callerName'] as String? ??
messageData['callerUserName'] as String? ??
messageData['userName'] as String? ??
'Unknown Caller';
final callerEmployeeNumber = messageData['callerEmployeeNumber'] as String? ?? callerId; final callerEmployeeNumber = messageData['callerEmployeeNumber'] as String? ?? callerId;
@ -225,7 +246,8 @@ class FirebaseNotificationManger {
return false; return false;
}(); }();
final moduleId = messageData['moduleId'] as String? ?? messageData['applicationId']?.toString(); final moduleId = messageData['moduleId'] as String? ??
messageData['applicationId']?.toString();
final referenceId = messageData['referenceId'] as String?; final referenceId = messageData['referenceId'] as String?;
final conversationId = messageData['conversationId'] as String?; final conversationId = messageData['conversationId'] as String?;
@ -410,6 +432,7 @@ class FirebaseNotificationManger {
} }
static initialized(BuildContext context) async { static initialized(BuildContext context) async {
//TOD0 add platform check here also //TOD0 add platform check here also
if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) { if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) {
log('📱 [INIT] Using Huawei Push Services', name: 'FirebaseNotificationManger'); log('📱 [INIT] Using Huawei Push Services', name: 'FirebaseNotificationManger');
@ -492,7 +515,8 @@ class FirebaseNotificationManger {
FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { FirebaseMessaging.instance.getInitialMessage().then((initialMessage) {
if (initialMessage != null) { if (initialMessage != null) {
// Check if it's a call notification // Check if it's a call notification
final notificationType = initialMessage.data['notificationType'] as String? ?? initialMessage.data['type'] as String?; final notificationType = initialMessage.data['notificationType'] as String? ??
initialMessage.data['type'] as String?;
final transactionType = initialMessage.data['transactionType'] as String?; final transactionType = initialMessage.data['transactionType'] as String?;
log('📱 [FCM_INITIAL] Notification Type: $notificationType', name: 'FirebaseNotificationManger'); log('📱 [FCM_INITIAL] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
log('📱 [FCM_INITIAL] Transaction Type: $transactionType', name: 'FirebaseNotificationManger'); log('📱 [FCM_INITIAL] Transaction Type: $transactionType', name: 'FirebaseNotificationManger');
@ -521,7 +545,8 @@ class FirebaseNotificationManger {
log('📱 [FCM_FOREGROUND] Body: ${message.notification?.body}', name: 'FirebaseNotificationManger'); log('📱 [FCM_FOREGROUND] Body: ${message.notification?.body}', name: 'FirebaseNotificationManger');
// FIXED: Check if it's a call notification - CHECK BOTH 'notificationType' AND 'type' // FIXED: Check if it's a call notification - CHECK BOTH 'notificationType' AND 'type'
final notificationType = message.data['notificationType'] as String? ?? message.data['type'] as String?; // Backend uses 'type' final notificationType = message.data['notificationType'] as String? ??
message.data['type'] as String?; // Backend uses 'type'
log('📱 [FCM_FOREGROUND] Notification Type: $notificationType', name: 'FirebaseNotificationManger'); log('📱 [FCM_FOREGROUND] Notification Type: $notificationType', name: 'FirebaseNotificationManger');
if (notificationType == 'incoming_call') { if (notificationType == 'incoming_call') {
@ -538,11 +563,19 @@ class FirebaseNotificationManger {
log(' - applicationId: ${message.data['applicationId']}', name: 'FirebaseNotificationManger'); log(' - applicationId: ${message.data['applicationId']}', name: 'FirebaseNotificationManger');
// CRITICAL FIX: Show CallKit UI in foreground // CRITICAL FIX: Show CallKit UI in foreground
final callId = message.data['callId'] as String? ?? message.data['sessionId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString(); final callId = message.data['callId'] as String? ??
message.data['sessionId'] as String? ??
DateTime.now().millisecondsSinceEpoch.toString();
final callerId = message.data['callerId'] as String? ?? message.data['callerEmployeeNumber'] as String? ?? message.data['sourceUserId'] as String? ?? 'unknown'; final callerId = message.data['callerId'] as String? ??
message.data['callerEmployeeNumber'] as String? ??
message.data['sourceUserId'] as String? ??
'unknown';
final callerName = message.data['callerName'] as String? ?? message.data['callerUserName'] as String? ?? message.data['userName'] as String? ?? 'Unknown Caller'; final callerName = message.data['callerName'] as String? ??
message.data['callerUserName'] as String? ??
message.data['userName'] as String? ??
'Unknown Caller';
final callerEmployeeNumber = message.data['callerEmployeeNumber'] as String? ?? callerId; final callerEmployeeNumber = message.data['callerEmployeeNumber'] as String? ?? callerId;
@ -554,7 +587,8 @@ class FirebaseNotificationManger {
return false; return false;
}(); }();
final moduleId = message.data['moduleId'] as String? ?? message.data['applicationId']?.toString(); final moduleId = message.data['moduleId'] as String? ??
message.data['applicationId']?.toString();
final referenceId = message.data['referenceId'] as String?; final referenceId = message.data['referenceId'] as String?;
final conversationId = message.data['conversationId'] as String?; final conversationId = message.data['conversationId'] as String?;
@ -632,7 +666,11 @@ class FirebaseNotificationManger {
if (Platform.isAndroid) { if (Platform.isAndroid) {
if (message.data["notificationType"] != 'NurseConfirmArrive') { if (message.data["notificationType"] != 'NurseConfirmArrive') {
NotificationManger.showNotification( NotificationManger.showNotification(
title: message.notification?.title ?? "", subtext: message.notification?.body ?? "", hashcode: int.tryParse("1234") ?? 1, payload: json.encode(message.data), context: context); title: message.notification?.title ?? "",
subtext: message.notification?.body ?? "",
hashcode: int.tryParse("1234") ?? 1,
payload: json.encode(message.data),
context: context);
} }
} }
return; return;

@ -37,7 +37,6 @@ 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/call_manager.dart';
import 'package:test_sa/modules/cx_module/chat/services/signalr_service.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';
@ -229,7 +228,7 @@ class MyApp extends StatelessWidget {
return MultiProvider( return MultiProvider(
providers: [ providers: [
// ============================================================ // ============================================================
// CORE PROVIDERS (11) - Always instantiated at app launch // CORE PROVIDERS (10) - Always instantiated at app launch
// These are critical for app functionality // These are critical for app functionality
// ============================================================ // ============================================================
ChangeNotifierProvider(create: (_) => UserProvider()), ChangeNotifierProvider(create: (_) => UserProvider()),
@ -242,8 +241,6 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => DepartmentsProvider()), ChangeNotifierProvider(create: (_) => DepartmentsProvider()),
ChangeNotifierProvider(create: (_) => NullableLoadingProvider()), ChangeNotifierProvider(create: (_) => NullableLoadingProvider()),
ChangeNotifierProvider(create: (_) => ChatProvider()), ChangeNotifierProvider(create: (_) => ChatProvider()),
// HYBRID: CallManager uses Provider for UI, static instance for background calls
ChangeNotifierProvider(create: (_) => CallManager()),
// ============================================================ // ============================================================
// LAZY LOADED PROVIDERS (107) - Created only when accessed // LAZY LOADED PROVIDERS (107) - Created only when accessed

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:provider/provider.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';
@ -23,8 +22,7 @@ class _AudioCallPageState extends State<AudioCallPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// HYBRID: Get CallManager from Provider (UI layer) _callManager = CallManager();
_callManager = Provider.of<CallManager>(context, listen: false);
// Listen for call end and navigate back // Listen for call end and navigate back
_callManager.addListener(_checkCallStatus); _callManager.addListener(_checkCallStatus);

@ -3,7 +3,7 @@ import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:provider/provider.dart'; import 'package:test_sa/core/di/service_locator.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';
@ -27,8 +27,8 @@ class _VideoCallPageState extends State<VideoCallPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// HYBRID: Get CallManager from Provider (UI layer) _callManager = getIt<CallManager>();
_callManager = Provider.of<CallManager>(context, listen: false); // _callManager = CallManager();
// Listen for call end // Listen for call end
_callManager.addListener(_checkCallStatus); _callManager.addListener(_checkCallStatus);

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:permission_handler/permission_handler.dart' as PermissionHandler;
import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -26,7 +26,6 @@ import 'package:test_sa/modules/cx_module/chat/view_all_attachment_page.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/dialogs/dialog.dart';
import 'package:test_sa/views/widgets/sound/sound_player.dart'; import 'package:test_sa/views/widgets/sound/sound_player.dart';
import 'helper/chat_audio_player.dart'; import 'helper/chat_audio_player.dart';
@ -207,18 +206,7 @@ class _ChatPageState extends State<ChatPage> {
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: widget.title, title: widget.title,
actions: [ actions: [
Selector<ChatProvider, _ChatConnectionState>( // NEW: Call readiness indicator - shows if device can receive calls
selector: (_, provider) => _ChatConnectionState(
isLoading: provider.chatLoginTokenLoading,
loginResponse: provider.chatLoginResponse,
),
builder: (context, connectionState, child) {
if (!connectionState.isLoading && connectionState.loginResponse == null) {
return SizedBox();
} else {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (kDebugMode) if (kDebugMode)
Selector<ChatProvider, bool>( Selector<ChatProvider, bool>(
selector: (_, provider) => provider.areCallHandlersRegistered, selector: (_, provider) => provider.areCallHandlersRegistered,
@ -240,37 +228,8 @@ class _ChatPageState extends State<ChatPage> {
selector: (_, provider) => provider.recipient, selector: (_, provider) => provider.recipient,
builder: (context, recipient, _) => IconButton( builder: (context, recipient, _) => IconButton(
icon: "calling_icon".toSvgAsset(width: 24, height: 24), icon: "calling_icon".toSvgAsset(width: 24, height: 24),
onPressed: () async{ onPressed: () {
bool audioPermission = await Provider.of<ChatProvider>(context, listen: false).checkAudioPermissions(); log('recipient: ${recipient?.userName}');
if (!audioPermission) {
context.showConfirmDialog("To perform audio call, we need permission to access microphone.", title: "Permission Required", onTap: () async {
Navigator.pop(context);
audioPermission = await Provider.of<ChatProvider>(context, listen: false).requestAudioPermissions();
if (!audioPermission) {
bool openSetting = await showDialog(
context: context,
builder: (_) => const AAlertDialog(
title: "Permission denied",
content: "Permission is denied. Please go to application Setting to enable it.",
button1Text: "Cancel",
button2Text: "Open Setting",
),
);
if (!openSetting) PermissionHandler.openAppSettings();
return;
} else {
if (recipient != null) {
chatProvider.startCall(recipient, CallType.audio);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AudioCallPage(),
),
);
}
}
});
} else {
if (recipient != null) { if (recipient != null) {
chatProvider.startCall(recipient, CallType.audio); chatProvider.startCall(recipient, CallType.audio);
Navigator.push( Navigator.push(
@ -280,23 +239,7 @@ class _ChatPageState extends State<ChatPage> {
), ),
); );
} }
}
}, },
// {
// log('recipient: ${recipient?.userName}');
//
//
// if (recipient != null) {
// chatProvider.startCall(recipient, CallType.audio);
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const AudioCallPage(),
// ),
// );
// }
// },
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
), ),
@ -324,10 +267,6 @@ class _ChatPageState extends State<ChatPage> {
), ),
), ),
], ],
);
}
}),
],
), ),
// OPTIMIZATION: Using Selector for connection state (rarely changes) // OPTIMIZATION: Using Selector for connection state (rarely changes)
// This prevents rebuilding entire chat UI on every message/typing event // This prevents rebuilding entire chat UI on every message/typing event

@ -97,23 +97,14 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
CallManager get _callManager => getIt<CallManager>(); CallManager get _callManager => getIt<CallManager>();
CallStatus get callStatus => _callManager.callStatus; CallStatus get callStatus => _callManager.callStatus;
CallSession? get currentCall => _callManager.currentCall; CallSession? get currentCall => _callManager.currentCall;
Duration get callDuration => _callManager.callDuration; Duration get callDuration => _callManager.callDuration;
bool get isMuted => _callManager.isMuted; bool get isMuted => _callManager.isMuted;
bool get isSpeakerOn => _callManager.isSpeakerOn; bool get isSpeakerOn => _callManager.isSpeakerOn;
bool get isCameraOn => _callManager.isCameraOn; bool get isCameraOn => _callManager.isCameraOn;
bool get isPeerMuted => _callManager.isPeerMuted; bool get isPeerMuted => _callManager.isPeerMuted;
bool get isPeerCameraOn => _callManager.isPeerCameraOn; bool get isPeerCameraOn => _callManager.isPeerCameraOn;
bool get isCallInProgress => _callManager.isCallInProgress; bool get isCallInProgress => _callManager.isCallInProgress;
WebRTCService? get webrtcService => _callManager.webrtcService; WebRTCService? get webrtcService => _callManager.webrtcService;
// For backwards compatibility with UI components // For backwards compatibility with UI components
@ -129,7 +120,6 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
log(' [CALL STATUS] Call status is now managed by CallManager', name: 'ChatProvider'); log(' [CALL STATUS] Call status is now managed by CallManager', name: 'ChatProvider');
notifyListeners(); notifyListeners();
} }
/// OPTIMIZATION: Improved connection disposal to prevent memory leaks /// OPTIMIZATION: Improved connection disposal to prevent memory leaks
/// This properly handles errors and ensures connection is always cleaned up /// This properly handles errors and ensures connection is always cleaned up
Future<void> _disposeConnection() async { Future<void> _disposeConnection() async {
@ -199,21 +189,6 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// } // }
// } // }
Future<bool> checkAudioPermissions() => Permission.microphone.isGranted;
Future<bool> checkVideoPermissions() async => (await Permission.microphone.isGranted) && (await Permission.camera.isGranted);
Future<bool> requestAudioPermissions() async {
var result = await [Permission.microphone].request();
return (result[Permission.microphone] == PermissionStatus.granted);
}
Future<bool> requestVideoPermissions() async {
var result = await [Permission.microphone, Permission.camera].request();
return (result[Permission.microphone] == PermissionStatus.granted) && (result[Permission.camera] == PermissionStatus.granted);
}
Future<void> getUserAutoLoginTokenSilent(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber, {bool isMounted = true}) async { Future<void> getUserAutoLoginTokenSilent(int moduleId, int requestId, String title, String myId, String assigneeEmployeeNumber, {bool isMounted = true}) async {
// OPTIMIZATION: Use async reset for proper cleanup // OPTIMIZATION: Use async reset for proper cleanup
await reset(); await reset();
@ -255,6 +230,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
myEmployeeNumber: myId, myEmployeeNumber: myId,
); );
} }
} catch (ex) { } catch (ex) {
if (kDebugMode) { if (kDebugMode) {
print('⚠️ Error in getUserAutoLoginTokenSilent: $ex'); print('⚠️ Error in getUserAutoLoginTokenSilent: $ex');
@ -413,6 +389,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// CRITICAL FIX: Reference the singleton connection, don't create a new one // CRITICAL FIX: Reference the singleton connection, don't create a new one
chatHubConnection = signalRService.hubConnection; chatHubConnection = signalRService.hubConnection;
_registerChatEventHandlers(); _registerChatEventHandlers();
} catch (e, stackTrace) { } catch (e, stackTrace) {
if (kDebugMode) { if (kDebugMode) {
print('⚠️ Error building SignalR connection: $e'); print('⚠️ Error building SignalR connection: $e');
@ -676,8 +653,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// ==================== UTILITY METHODS ==================== // ==================== UTILITY METHODS ====================
/// Reset unread message count /// Reset unread message count
Future<bool> resetCount({int? moduleId, int? referenceNo, String? userId}) async { Future<bool> resetCount({int? moduleId, int? referenceNo, String? userId}) async { // Fixed: userId is String? not int?
// Fixed: userId is String? not int?
try { try {
if (chatLoginResponse != null && sender != null && recipient != null) { if (chatLoginResponse != null && sender != null && recipient != null) {
await ChatApiClient().resetCountApi( await ChatApiClient().resetCountApi(
@ -702,8 +678,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
} }
/// Upload attachments /// Upload attachments
Future<List<ChatAttachment>?> uploadAttachments(String username, File file, String conversationId) async { Future<List<ChatAttachment>?> uploadAttachments(String username, File file, String conversationId) async { // Fixed: match actual usage
// Fixed: match actual usage
try { try {
if (chatLoginResponse == null || chatParticipantModel == null) { if (chatLoginResponse == null || chatParticipantModel == null) {
if (kDebugMode) { if (kDebugMode) {
@ -736,8 +711,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
} }
/// Get unread messages /// Get unread messages
Future<List<UnReadMessage>> getUnReadMessages(String employeeId) async { Future<List<UnReadMessage>> getUnReadMessages(String employeeId) async { // Fixed: accept employeeId and return List<UnReadMessage>
// Fixed: accept employeeId and return List<UnReadMessage>
try { try {
if (chatLoginResponse == null) { if (chatLoginResponse == null) {
if (kDebugMode) { if (kDebugMode) {

@ -19,47 +19,40 @@ import 'package:test_sa/modules/cx_module/chat/services/pending_call_storage.dar
import 'package:test_sa/modules/cx_module/chat/services/call_coordinator.dart'; import 'package:test_sa/modules/cx_module/chat/services/call_coordinator.dart';
class CallManager extends ChangeNotifier { class CallManager extends ChangeNotifier {
static CallManager? _staticInstance;
static final CallManager _instance = CallManager._internal(); static final CallManager _instance = CallManager._internal();
factory CallManager() { factory CallManager() => _instance;
_staticInstance = _instance;
return _instance;
}
CallManager._internal(); CallManager._internal();
static CallManager get instance { // Services - Retrieved from DI
if (_staticInstance == null) {
throw StateError('CallManager not initialized. Ensure Provider<CallManager> is registered in main.dart');
}
return _staticInstance!;
}
static bool get isInitialized => _staticInstance != null;
SignalRService get _signalRService => getIt<SignalRService>(); SignalRService get _signalRService => getIt<SignalRService>();
WebRTCService get _webrtcService => getIt<WebRTCService>(); WebRTCService get _webrtcService => getIt<WebRTCService>();
final CallKitService _callKitService = CallKitService(); final CallKitService _callKitService = CallKitService();
// Public getter for WebRTC service (used by call pages and ChatProvider)
WebRTCService get webrtcService => _webrtcService; WebRTCService get webrtcService => _webrtcService;
// Call state
CallSession? _currentCall; CallSession? _currentCall;
CallStatus _callStatus = CallStatus.idle; CallStatus _callStatus = CallStatus.idle;
Duration _callDuration = Duration.zero; Duration _callDuration = Duration.zero;
Timer? _callDurationTimer; Timer? _callDurationTimer;
Timer? _callTimeoutTimer; Timer? _callTimeoutTimer;
// Call controls
bool _isMuted = false; bool _isMuted = false;
bool _isSpeakerOn = false; bool _isSpeakerOn = false;
bool _isCameraOn = true; bool _isCameraOn = true;
bool _isPeerMuted = false; bool _isPeerMuted = false;
bool _isPeerCameraOn = true; bool _isPeerCameraOn = true;
// Initialization state
bool _callKitInitialized = false; bool _callKitInitialized = false;
bool _handlersRegistered = false; bool _handlersRegistered = false;
// Module context (for SignalR invocations)
String? _userId; String? _userId;
String? _authToken; String? _authToken;
String? _moduleId; String? _moduleId;
@ -67,17 +60,26 @@ class CallManager extends ChangeNotifier {
String? _conversationId; String? _conversationId;
String? _myEmployeeNumber; String? _myEmployeeNumber;
// CRITICAL FIX: Static credential cache for background calls
// This allows incoming calls to work even when app is in background
static String? _cachedUserId; static String? _cachedUserId;
static String? _cachedAuthToken; static String? _cachedAuthToken;
static String? _cachedEmployeeNumber; static String? _cachedEmployeeNumber;
// CRITICAL: Pending call data for background acceptance
// This preserves the COMPLETE notification payload
Map<String, dynamic>? _pendingCallData; Map<String, dynamic>? _pendingCallData;
// CRITICAL FIX: Navigation pending flag for background calls
// When app is in background and call is accepted, we set this flag
// The main UI will check this flag when it resumes and navigate
bool _navigationPending = false; bool _navigationPending = false;
// CRITICAL FIX: WebRTC initialization synchronization
Completer<void>? _webrtcInitCompleter; Completer<void>? _webrtcInitCompleter;
String? _pendingOfferSdp; String? _pendingOfferSdp; // Queue for offer that arrives during initialization
// Getters
CallSession? get currentCall => _currentCall; CallSession? get currentCall => _currentCall;
CallStatus get callStatus => _callStatus; CallStatus get callStatus => _callStatus;
@ -96,8 +98,10 @@ class CallManager extends ChangeNotifier {
bool get isCallInProgress => _callStatus != CallStatus.idle; bool get isCallInProgress => _callStatus != CallStatus.idle;
// CRITICAL: Public getter for navigation pending flag
bool get isNavigationPending => _navigationPending; bool get isNavigationPending => _navigationPending;
/// Initialize CallManager with user credentials
Future<void> initialize({ Future<void> initialize({
required String userId, required String userId,
required String authToken, required String authToken,
@ -107,11 +111,14 @@ class CallManager extends ChangeNotifier {
String? employeeNumber, String? employeeNumber,
}) async { }) async {
try { try {
// CRITICAL FIX: Cache credentials for background call handling
_cachedUserId = userId; _cachedUserId = userId;
_cachedAuthToken = authToken; _cachedAuthToken = authToken;
_cachedEmployeeNumber = employeeNumber; _cachedEmployeeNumber = employeeNumber;
// CRITICAL FIX: Don't re-initialize if already initialized with same USER credentials
if (_userId == userId && _authToken == authToken && _myEmployeeNumber?.toLowerCase() == employeeNumber?.toLowerCase() && _handlersRegistered) { if (_userId == userId && _authToken == authToken && _myEmployeeNumber?.toLowerCase() == employeeNumber?.toLowerCase() && _handlersRegistered) {
// User is the same, just update conversation context without reconnecting
final conversationChanged = _conversationId != conversationId; final conversationChanged = _conversationId != conversationId;
final moduleChanged = _moduleId != moduleId; final moduleChanged = _moduleId != moduleId;
final referenceChanged = _referenceId != referenceId; final referenceChanged = _referenceId != referenceId;
@ -137,6 +144,7 @@ class CallManager extends ChangeNotifier {
return; return;
} }
// Store module context
_userId = userId; _userId = userId;
_authToken = authToken; _authToken = authToken;
_moduleId = moduleId; _moduleId = moduleId;
@ -144,16 +152,23 @@ class CallManager extends ChangeNotifier {
_conversationId = conversationId; _conversationId = conversationId;
_myEmployeeNumber = employeeNumber; _myEmployeeNumber = employeeNumber;
final connected = await _signalRService.initialize(userId: userId, authToken: authToken, conversationId: conversationId); // Initialize SignalR connection
final connected = await _signalRService.initialize(
userId: userId,
authToken: authToken,
conversationId: conversationId,
);
if (!connected) { if (!connected) {
throw Exception('Failed to initialize SignalR connection'); throw Exception('Failed to initialize SignalR connection');
} }
// Initialize CallKit
if (!_callKitInitialized) { if (!_callKitInitialized) {
await _initializeCallKit(); await _initializeCallKit();
} }
// Register call event handlers
if (!_handlersRegistered) { if (!_handlersRegistered) {
_registerCallHandlers(); _registerCallHandlers();
} }
@ -163,6 +178,7 @@ class CallManager extends ChangeNotifier {
} }
} }
/// Initialize CallKit service
Future<void> _initializeCallKit() async { Future<void> _initializeCallKit() async {
if (_callKitInitialized) { if (_callKitInitialized) {
return; return;
@ -423,8 +439,17 @@ class CallManager extends ChangeNotifier {
/// Accept incoming call /// Accept incoming call
Future<void> acceptCall() async { Future<void> acceptCall() async {
try { try {
log('═══════════════════════════════════════════', name: 'CallManager');
log('✅ [ACCEPT] ▶️ ACCEPT CALL STARTED', name: 'CallManager');
log('✅ [ACCEPT] Timestamp: ${DateTime.now().toIso8601String()}', name: 'CallManager');
// CRITICAL: Handle background acceptance scenario
if (_currentCall == null && _pendingCallData != null) { if (_currentCall == null && _pendingCallData != null) {
log('🔧 [ACCEPT] No current call but pending data exists - restoring from background', name: 'CallManager');
log(' Pending call ID: ${_pendingCallData!['callId']}', name: 'CallManager');
log(' Pending caller: ${_pendingCallData!['callerName']}', name: 'CallManager');
// Restore call session from pending data
_currentCall = CallSession( _currentCall = CallSession(
callId: _pendingCallData!['callId'] as String, callId: _pendingCallData!['callId'] as String,
type: (_pendingCallData!['isVideoCall'] as bool) ? CallType.video : CallType.audio, type: (_pendingCallData!['isVideoCall'] as bool) ? CallType.video : CallType.audio,
@ -434,10 +459,14 @@ class CallManager extends ChangeNotifier {
peerAvatar: null, peerAvatar: null,
startTime: DateTime.now(), startTime: DateTime.now(),
); );
_updateCallStatus(CallStatus.incomingRinging); _updateCallStatus(CallStatus.incomingRinging);
log('✅ [ACCEPT] Call session restored from pending data', name: 'CallManager');
// Ensure CallManager is initialized with pending call context // Ensure CallManager is initialized with pending call context
if (_userId == null || _authToken == null) { if (_userId == null || _authToken == null) {
log('⚠️ [ACCEPT] CallManager not initialized, attempting auto-initialization...', name: 'CallManager');
if (_cachedUserId != null && _cachedAuthToken != null && _cachedEmployeeNumber != null) { if (_cachedUserId != null && _cachedAuthToken != null && _cachedEmployeeNumber != null) {
await initialize( await initialize(
userId: _cachedUserId!, userId: _cachedUserId!,
@ -447,6 +476,7 @@ class CallManager extends ChangeNotifier {
referenceId: _pendingCallData!['referenceId'] as String?, referenceId: _pendingCallData!['referenceId'] as String?,
employeeNumber: _cachedEmployeeNumber, employeeNumber: _cachedEmployeeNumber,
); );
log('✅ [ACCEPT] CallManager initialized for background call', name: 'CallManager');
} else { } else {
log('❌ [ACCEPT] Cannot initialize - no cached credentials', name: 'CallManager'); log('❌ [ACCEPT] Cannot initialize - no cached credentials', name: 'CallManager');
await _cleanup(); await _cleanup();
@ -735,7 +765,9 @@ class CallManager extends ChangeNotifier {
_setupWebRTCCallbacks(); _setupWebRTCCallbacks();
// Initialize with timeout protection // Initialize with timeout protection
final initFuture = _currentCall!.type == CallType.audio ? _webrtcService.initializeForAudioCall() : _webrtcService.initializeForVideoCall(); final initFuture = _currentCall!.type == CallType.audio
? _webrtcService.initializeForAudioCall()
: _webrtcService.initializeForVideoCall();
// Add 10 second timeout for initialization // Add 10 second timeout for initialization
await initFuture.timeout( await initFuture.timeout(

@ -9,10 +9,8 @@ import 'package:test_sa/new_views/app_style/app_color.dart';
class AAlertDialog extends StatelessWidget { class AAlertDialog extends StatelessWidget {
final String? title; final String? title;
final String? content; final String? content;
final String? button1Text;
final String? button2Text;
const AAlertDialog({Key? key, this.title, this.content, this.button1Text, this.button2Text}) : super(key: key); const AAlertDialog({Key? key, this.title, this.content}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,14 +21,14 @@ class AAlertDialog extends StatelessWidget {
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
// child: Text(_subtitle.confirm), // child: Text(_subtitle.confirm),
child: Text(button1Text ?? context.translation.confirm), child: Text(context.translation.confirm),
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
}, },
), ),
TextButton( TextButton(
// child: Text(_subtitle.cancel), // child: Text(_subtitle.cancel),
child: Text(button2Text ?? context.translation.cancel), child: Text(context.translation.cancel),
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
}, },
@ -39,21 +37,20 @@ class AAlertDialog extends StatelessWidget {
) )
: AlertDialog( : AlertDialog(
backgroundColor: AppColor.background(context), backgroundColor: AppColor.background(context),
actionsPadding: const EdgeInsets.only(bottom: 12, left: 24, right: 24),
title: title != null ? Text(title!) : null, title: title != null ? Text(title!) : null,
content: content != null ? Text(content!) : null, content: content != null ? Text(content!) : null,
// contentTextStyle: AppTextStyles.bodyText.copyWith(color: (context.isDark ? AppColor.neutral30 : AppColor.neutral50)), // contentTextStyle: AppTextStyles.bodyText.copyWith(color: (context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
// child: Text(_subtitle.confirm), // child: Text(_subtitle.confirm),
child: Text(button1Text ?? context.translation.confirm, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))), child: Text(context.translation.confirm, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))),
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
}, },
), ),
TextButton( TextButton(
// child: Text(_subtitle.cancel), // child: Text(_subtitle.cancel),
child: Text(button2Text ?? context.translation.cancel, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))), child: Text(context.translation.cancel, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))),
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
}, },

Loading…
Cancel
Save