Compare commits

..

3 Commits

@ -0,0 +1,5 @@
<?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,17 +4,18 @@ 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,28 +26,19 @@ 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? ?? final notificationType = message.data['notificationType'] as String? ?? message.data['type'] 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');
// 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? ?? final callId = message.data['callId'] as String? ?? message.data['sessionId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString();
message.data['sessionId'] as String? ??
DateTime.now().millisecondsSinceEpoch.toString();
final callerId = message.data['callerId'] as String? ?? final callerId = message.data['callerId'] as String? ?? message.data['callerEmployeeNumber'] as String? ?? message.data['sourceUserId'] as String? ?? 'unknown';
message.data['callerEmployeeNumber'] as String? ??
message.data['sourceUserId'] as String? ??
'unknown';
final callerName = message.data['callerName'] as String? ?? final callerName = message.data['callerName'] as String? ?? message.data['callerUserName'] as String? ?? message.data['userName'] as String? ?? 'Unknown Caller';
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;
@ -59,14 +50,13 @@ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
return false; return false;
}(); }();
final moduleId = message.data['moduleId'] as String? ?? final moduleId = message.data['moduleId'] as String? ?? message.data['applicationId']?.toString();
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?;
// CRITICAL FIX: DO NOT initialize any communication services in background isolate // CRITICAL FIX: DO NOT initialize any communication services in background isolate
// Only persist call data and show CallKit // Only persist call data and show CallKit
///TODO need to find other solution to this after testing ///TODO need to find other solution to this after testing
await PendingCallStorage.savePendingCall( await PendingCallStorage.savePendingCall(
callId: callId, callId: callId,
callerId: callerId, callerId: callerId,
@ -141,8 +131,7 @@ 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', log('❌ [BACKGROUND] Error handling background message: $e', name: 'FirebaseNotificationManger', error: e, stackTrace: stackTrace);
name: 'FirebaseNotificationManger', error: e, stackTrace: stackTrace);
} }
} }
@ -188,8 +177,7 @@ 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');
@ -214,27 +202,18 @@ 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? ?? final notificationType = messageData['notificationType'] as String? ?? messageData['type'] as String?; // Backend uses 'type'
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? ?? final callId = messageData['callId'] as String? ?? messageData['sessionId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString();
messageData['sessionId'] as String? ??
DateTime.now().millisecondsSinceEpoch.toString();
final callerId = messageData['callerId'] as String? ?? final callerId = messageData['callerId'] as String? ?? messageData['callerEmployeeNumber'] as String? ?? messageData['sourceUserId'] as String? ?? 'unknown';
messageData['callerEmployeeNumber'] as String? ??
messageData['sourceUserId'] as String? ??
'unknown';
final callerName = messageData['callerName'] as String? ?? final callerName = messageData['callerName'] as String? ?? messageData['callerUserName'] as String? ?? messageData['userName'] as String? ?? 'Unknown Caller';
messageData['callerUserName'] as String? ??
messageData['userName'] as String? ??
'Unknown Caller';
final callerEmployeeNumber = messageData['callerEmployeeNumber'] as String? ?? callerId; final callerEmployeeNumber = messageData['callerEmployeeNumber'] as String? ?? callerId;
@ -246,8 +225,7 @@ class FirebaseNotificationManger {
return false; return false;
}(); }();
final moduleId = messageData['moduleId'] as String? ?? final moduleId = messageData['moduleId'] as String? ?? messageData['applicationId']?.toString();
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?;
@ -432,7 +410,6 @@ 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');
@ -515,8 +492,7 @@ 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? ?? final notificationType = initialMessage.data['notificationType'] as String? ?? initialMessage.data['type'] 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');
@ -545,8 +521,7 @@ 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? ?? final notificationType = message.data['notificationType'] as String? ?? message.data['type'] as String?; // Backend uses 'type'
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') {
@ -563,19 +538,11 @@ 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? ?? final callId = message.data['callId'] as String? ?? message.data['sessionId'] as String? ?? DateTime.now().millisecondsSinceEpoch.toString();
message.data['sessionId'] as String? ??
DateTime.now().millisecondsSinceEpoch.toString();
final callerId = message.data['callerId'] as String? ?? final callerId = message.data['callerId'] as String? ?? message.data['callerEmployeeNumber'] as String? ?? message.data['sourceUserId'] as String? ?? 'unknown';
message.data['callerEmployeeNumber'] as String? ??
message.data['sourceUserId'] as String? ??
'unknown';
final callerName = message.data['callerName'] as String? ?? final callerName = message.data['callerName'] as String? ?? message.data['callerUserName'] as String? ?? message.data['userName'] as String? ?? 'Unknown Caller';
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;
@ -587,8 +554,7 @@ class FirebaseNotificationManger {
return false; return false;
}(); }();
final moduleId = message.data['moduleId'] as String? ?? final moduleId = message.data['moduleId'] as String? ?? message.data['applicationId']?.toString();
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?;
@ -666,16 +632,12 @@ 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 ?? "", title: message.notification?.title ?? "", subtext: message.notification?.body ?? "", hashcode: int.tryParse("1234") ?? 1, payload: json.encode(message.data), context: context);
subtext: message.notification?.body ?? "",
hashcode: int.tryParse("1234") ?? 1,
payload: json.encode(message.data),
context: context);
} }
} }
return; return;
}); });
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
log('📱 [FCM_TAP] Notification tapped', name: 'FirebaseNotificationManger'); log('📱 [FCM_TAP] Notification tapped', name: 'FirebaseNotificationManger');
@ -697,7 +659,7 @@ class FirebaseNotificationManger {
} }
log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger'); log('═══════════════════════════════════════════', name: 'FirebaseNotificationManger');
}); });
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
log('✅ [INIT] FirebaseNotificationManger initialized successfully', name: 'FirebaseNotificationManger'); log('✅ [INIT] FirebaseNotificationManger initialized successfully', name: 'FirebaseNotificationManger');

@ -37,6 +37,7 @@ import 'package:test_sa/modules/asset_inventory_module/provider/asset_inventory_
import 'package:test_sa/modules/cm_module/cm_detail_provider.dart'; import 'package:test_sa/modules/cm_module/cm_detail_provider.dart';
import 'package:test_sa/modules/cm_module/create_cm_request.dart'; import 'package:test_sa/modules/cm_module/create_cm_request.dart';
import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; import 'package:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:test_sa/modules/cx_module/chat/services/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';
@ -228,7 +229,7 @@ class MyApp extends StatelessWidget {
return MultiProvider( return MultiProvider(
providers: [ providers: [
// ============================================================ // ============================================================
// CORE PROVIDERS (10) - Always instantiated at app launch // CORE PROVIDERS (11) - Always instantiated at app launch
// These are critical for app functionality // These are critical for app functionality
// ============================================================ // ============================================================
ChangeNotifierProvider(create: (_) => UserProvider()), ChangeNotifierProvider(create: (_) => UserProvider()),
@ -241,6 +242,8 @@ 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,5 +1,6 @@
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';
@ -22,7 +23,8 @@ class _AudioCallPageState extends State<AudioCallPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_callManager = CallManager(); // HYBRID: Get CallManager from Provider (UI layer)
_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:test_sa/core/di/service_locator.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';
@ -27,8 +27,8 @@ class _VideoCallPageState extends State<VideoCallPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_callManager = getIt<CallManager>(); // HYBRID: Get CallManager from Provider (UI layer)
// _callManager = CallManager(); _callManager = Provider.of<CallManager>(context, listen: false);
// 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,6 +26,7 @@ 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';
@ -206,66 +207,126 @@ class _ChatPageState extends State<ChatPage> {
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: widget.title, title: widget.title,
actions: [ actions: [
// NEW: Call readiness indicator - shows if device can receive calls Selector<ChatProvider, _ChatConnectionState>(
if (kDebugMode) selector: (_, provider) => _ChatConnectionState(
Selector<ChatProvider, bool>( isLoading: provider.chatLoginTokenLoading,
selector: (_, provider) => provider.areCallHandlersRegistered, loginResponse: provider.chatLoginResponse,
builder: (context, handlersRegistered, _) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Center(
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: handlersRegistered ? Colors.green : Colors.red,
),
), ),
), builder: (context, connectionState, child) {
), if (!connectionState.isLoading && connectionState.loginResponse == null) {
), return SizedBox();
Selector<ChatProvider, Participants?>( } else {
selector: (_, provider) => provider.recipient, return Row(
builder: (context, recipient, _) => IconButton( mainAxisSize: MainAxisSize.min,
icon: "calling_icon".toSvgAsset(width: 24, height: 24), children: [
onPressed: () { if (kDebugMode)
log('recipient: ${recipient?.userName}'); Selector<ChatProvider, bool>(
if (recipient != null) { selector: (_, provider) => provider.areCallHandlersRegistered,
chatProvider.startCall(recipient, CallType.audio); builder: (context, handlersRegistered, _) => Padding(
Navigator.push( padding: const EdgeInsets.only(right: 8),
context, child: Center(
MaterialPageRoute( child: Container(
builder: (context) => const AudioCallPage(), width: 8,
), height: 8,
); decoration: BoxDecoration(
} shape: BoxShape.circle,
}, color: handlersRegistered ? Colors.green : Colors.red,
padding: EdgeInsets.zero, ),
constraints: const BoxConstraints(), ),
), ),
), ),
Selector<ChatProvider, Participants?>( ),
selector: (_, provider) => provider.recipient, Selector<ChatProvider, Participants?>(
builder: (context, recipient, _) => IconButton( selector: (_, provider) => provider.recipient,
icon: "video_call_icon".toSvgAsset( builder: (context, recipient, _) => IconButton(
width: 24, icon: "calling_icon".toSvgAsset(width: 24, height: 24),
height: 24, onPressed: () async{
), bool audioPermission = await Provider.of<ChatProvider>(context, listen: false).checkAudioPermissions();
onPressed: () { if (!audioPermission) {
if (recipient != null) { context.showConfirmDialog("To perform audio call, we need permission to access microphone.", title: "Permission Required", onTap: () async {
chatProvider.startCall(recipient, CallType.video); Navigator.pop(context);
Navigator.push( audioPermission = await Provider.of<ChatProvider>(context, listen: false).requestAudioPermissions();
context, if (!audioPermission) {
MaterialPageRoute( bool openSetting = await showDialog(
builder: (context) => const VideoCallPage(), 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) {
chatProvider.startCall(recipient, CallType.audio);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AudioCallPage(),
),
);
}
}
},
// {
// log('recipient: ${recipient?.userName}');
//
//
// if (recipient != null) {
// chatProvider.startCall(recipient, CallType.audio);
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const AudioCallPage(),
// ),
// );
// }
// },
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
Selector<ChatProvider, Participants?>(
selector: (_, provider) => provider.recipient,
builder: (context, recipient, _) => IconButton(
icon: "video_call_icon".toSvgAsset(
width: 24,
height: 24,
),
onPressed: () {
if (recipient != null) {
chatProvider.startCall(recipient, CallType.video);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const VideoCallPage(),
),
);
}
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
); );
} }
}, }),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
], ],
), ),
// OPTIMIZATION: Using Selector for connection state (rarely changes) // OPTIMIZATION: Using Selector for connection state (rarely changes)

@ -95,16 +95,25 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// These getters provide read-only access to call state for UI // These getters provide read-only access to call state for UI
// All call operations should go through CallManager directly // All call operations should go through CallManager directly
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
@ -120,6 +129,7 @@ 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 {
@ -127,10 +137,10 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// SignalR is now a SINGLETON managed by SignalRService // SignalR is now a SINGLETON managed by SignalRService
// It's shared between ChatProvider and CallManager // It's shared between ChatProvider and CallManager
// Closing it here would break ongoing calls and call setup // Closing it here would break ongoing calls and call setup
// Just clear the local reference // Just clear the local reference
_chatHubConnection = null; _chatHubConnection = null;
if (kDebugMode) { if (kDebugMode) {
print('🔌 [ChatProvider] Cleared local SignalR reference (connection remains active)'); print('🔌 [ChatProvider] Cleared local SignalR reference (connection remains active)');
} }
@ -141,7 +151,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// CRITICAL FIX: DO NOT dispose the SignalR connection // CRITICAL FIX: DO NOT dispose the SignalR connection
// Just clear ChatProvider's local state // Just clear ChatProvider's local state
// The SignalRService singleton will manage the connection lifecycle // The SignalRService singleton will manage the connection lifecycle
_chatHubConnection = null; _chatHubConnection = null;
@ -154,7 +164,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
sender = null; sender = null;
recipient = null; recipient = null;
ChatApiClient().chatLoginResponse = null; ChatApiClient().chatLoginResponse = null;
log('✅ [ChatProvider] Chat state reset (SignalR connection preserved)', name: 'ChatProvider'); log('✅ [ChatProvider] Chat state reset (SignalR connection preserved)', name: 'ChatProvider');
} }
@ -165,13 +175,13 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
// CRITICAL FIX: DO NOT close SignalR connection on dispose // CRITICAL FIX: DO NOT close SignalR connection on dispose
// The connection is a singleton and may be used by other parts of the app // The connection is a singleton and may be used by other parts of the app
// Only clear local state // Only clear local state
_chatHubConnection = null; _chatHubConnection = null;
if (kDebugMode) { if (kDebugMode) {
print('✅ ChatProvider disposed (SignalR connection preserved)'); print('✅ ChatProvider disposed (SignalR connection preserved)');
} }
super.dispose(); super.dispose();
} }
@ -189,6 +199,21 @@ 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();
@ -204,7 +229,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
log('✅ Got chatParticipantModel: ${chatParticipantModel?.toJson()}'); log('✅ Got chatParticipantModel: ${chatParticipantModel?.toJson()}');
try { try {
sender = chatParticipantModel?.participants?.firstWhere( sender = chatParticipantModel?.participants?.firstWhere(
(participant) => participant.userId?.toLowerCase() == myId.toLowerCase() (participant) => participant.userId?.toLowerCase() == myId.toLowerCase()
); );
} catch (e) { } catch (e) {
log('⚠️ Sender NOT FOUND for myId: $myId. Error: $e'); log('⚠️ Sender NOT FOUND for myId: $myId. Error: $e');
@ -213,7 +238,7 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
try { try {
recipient = chatParticipantModel?.participants?.firstWhere( recipient = chatParticipantModel?.participants?.firstWhere(
(participant) => participant.userId?.toLowerCase() == assigneeEmployeeNumber.toLowerCase() (participant) => participant.userId?.toLowerCase() == assigneeEmployeeNumber.toLowerCase()
); );
log('✅ recipient found: userId=${recipient?.userId}, userName=${recipient?.userName}, employeeNumber=${recipient?.employeeNumber}'); log('✅ recipient found: userId=${recipient?.userId}, userName=${recipient?.userName}, employeeNumber=${recipient?.employeeNumber}');
} catch (e) { } catch (e) {
@ -230,7 +255,6 @@ 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');
@ -389,7 +413,6 @@ 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');
@ -447,8 +470,8 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
final context = navigatorKey.currentContext; final context = navigatorKey.currentContext;
if (context != null) { if (context != null) {
CallErrorHandler.showGenericError( CallErrorHandler.showGenericError(
context, context,
'Unable to initialize calling system. Please try again.' 'Unable to initialize calling system. Please try again.'
); );
} }
return; return;
@ -653,7 +676,8 @@ 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 { // Fixed: userId is String? not int? Future<bool> resetCount({int? moduleId, int? referenceNo, String? userId}) async {
// 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(
@ -678,7 +702,8 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
} }
/// Upload attachments /// Upload attachments
Future<List<ChatAttachment>?> uploadAttachments(String username, File file, String conversationId) async { // Fixed: match actual usage Future<List<ChatAttachment>?> uploadAttachments(String username, File file, String conversationId) async {
// Fixed: match actual usage
try { try {
if (chatLoginResponse == null || chatParticipantModel == null) { if (chatLoginResponse == null || chatParticipantModel == null) {
if (kDebugMode) { if (kDebugMode) {
@ -711,7 +736,8 @@ class ChatProvider with ChangeNotifier, DiagnosticableTreeMixin {
} }
/// Get unread messages /// Get unread messages
Future<List<UnReadMessage>> getUnReadMessages(String employeeId) async { // Fixed: accept employeeId and return List<UnReadMessage> Future<List<UnReadMessage>> getUnReadMessages(String employeeId) async {
// Fixed: accept employeeId and return List<UnReadMessage>
try { try {
if (chatLoginResponse == null) { if (chatLoginResponse == null) {
if (kDebugMode) { if (kDebugMode) {

@ -19,40 +19,47 @@ 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() => _instance; factory CallManager() {
_staticInstance = _instance;
return _instance;
}
CallManager._internal(); CallManager._internal();
// Services - Retrieved from DI static CallManager get instance {
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;
@ -60,26 +67,17 @@ 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; // Queue for offer that arrives during initialization String? _pendingOfferSdp;
// Getters
CallSession? get currentCall => _currentCall; CallSession? get currentCall => _currentCall;
CallStatus get callStatus => _callStatus; CallStatus get callStatus => _callStatus;
@ -98,10 +96,8 @@ 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,
@ -111,14 +107,11 @@ 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;
@ -144,7 +137,6 @@ class CallManager extends ChangeNotifier {
return; return;
} }
// Store module context
_userId = userId; _userId = userId;
_authToken = authToken; _authToken = authToken;
_moduleId = moduleId; _moduleId = moduleId;
@ -152,23 +144,16 @@ class CallManager extends ChangeNotifier {
_conversationId = conversationId; _conversationId = conversationId;
_myEmployeeNumber = employeeNumber; _myEmployeeNumber = employeeNumber;
// Initialize SignalR connection final connected = await _signalRService.initialize(userId: userId, authToken: authToken, conversationId: conversationId);
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();
} }
@ -178,7 +163,6 @@ class CallManager extends ChangeNotifier {
} }
} }
/// Initialize CallKit service
Future<void> _initializeCallKit() async { Future<void> _initializeCallKit() async {
if (_callKitInitialized) { if (_callKitInitialized) {
return; return;
@ -439,17 +423,8 @@ 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,
@ -459,14 +434,10 @@ 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!,
@ -476,7 +447,6 @@ 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();
@ -765,9 +735,7 @@ class CallManager extends ChangeNotifier {
_setupWebRTCCallbacks(); _setupWebRTCCallbacks();
// Initialize with timeout protection // Initialize with timeout protection
final initFuture = _currentCall!.type == CallType.audio final initFuture = _currentCall!.type == CallType.audio ? _webrtcService.initializeForAudioCall() : _webrtcService.initializeForVideoCall();
? _webrtcService.initializeForAudioCall()
: _webrtcService.initializeForVideoCall();
// Add 10 second timeout for initialization // Add 10 second timeout for initialization
await initFuture.timeout( await initFuture.timeout(

@ -9,53 +9,56 @@ 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}) : super(key: key); const AAlertDialog({Key? key, this.title, this.content, this.button1Text, this.button2Text}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Platform.isIOS return Platform.isIOS
? CupertinoAlertDialog( ? CupertinoAlertDialog(
title: title != null ? Text(title!) : null, title: title != null ? Text(title!) : null,
content: content != null ? Text(content!) : null, content: content != null ? Text(content!) : null,
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
// child: Text(_subtitle.confirm), // child: Text(_subtitle.confirm),
child: Text(context.translation.confirm), child: Text(button1Text ?? 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(context.translation.cancel), child: Text(button2Text ?? context.translation.cancel),
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
}, },
), ),
], ],
) )
: AlertDialog( : AlertDialog(
backgroundColor: AppColor.background(context), backgroundColor: AppColor.background(context),
title: title != null ? Text(title!) : null, actionsPadding: const EdgeInsets.only(bottom: 12, left: 24, right: 24),
content: content != null ? Text(content!) : null, title: title != null ? Text(title!) : null,
// contentTextStyle: AppTextStyles.bodyText.copyWith(color: (context.isDark ? AppColor.neutral30 : AppColor.neutral50)), content: content != null ? Text(content!) : null,
actions: <Widget>[ // contentTextStyle: AppTextStyles.bodyText.copyWith(color: (context.isDark ? AppColor.neutral30 : AppColor.neutral50)),
TextButton( actions: <Widget>[
// child: Text(_subtitle.confirm), TextButton(
child: Text(context.translation.confirm, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))), // child: Text(_subtitle.confirm),
onPressed: () { child: Text(button1Text ?? context.translation.confirm, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))),
Navigator.of(context).pop(true); onPressed: () {
}, Navigator.of(context).pop(true);
), },
TextButton( ),
// child: Text(_subtitle.cancel), TextButton(
child: Text(context.translation.cancel, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))), // child: Text(_subtitle.cancel),
onPressed: () { child: Text(button2Text ?? context.translation.cancel, style: AppTextStyles.bodyText.copyWith(color: AppColor.loadingColor(context))),
Navigator.of(context).pop(false); onPressed: () {
}, Navigator.of(context).pop(false);
), },
], ),
); ],
);
} }
} }

Loading…
Cancel
Save