csn_flutter 1.0.0 copy "csn_flutter: ^1.0.0" to clipboard
csn_flutter: ^1.0.0 copied to clipboard

Flutter SDK for CSN realtime audio/video calling with prebuilt request-queue and call UI.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';

import 'package:csn_flutter/csn_flutter.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:shared_preferences/shared_preferences.dart';

const String _appName = 'Deafott Customer Support';

enum _StaffRole {
  admin,
  executive,
}

const String _sessionTokenKey = 'csn_session_token';
const String _sessionRoleKey = 'csn_session_role';
const String _sessionUserIdKey = 'csn_session_user_id';
const String _sessionUserNameKey = 'csn_session_user_name';
const String _sessionUserEmailKey = 'csn_session_user_email';

String _formatDurationSeconds(int? seconds) {
  final total = seconds ?? 0;
  final minutes = total ~/ 60;
  final remainingSeconds = total % 60;
  if (minutes <= 0) return '${remainingSeconds}s';
  return '${minutes}m ${remainingSeconds.toString().padLeft(2, '0')}s';
}

String _formatExactTime(String? isoValue) {
  if (isoValue == null || isoValue.trim().isEmpty) return '-';
  final parsed = DateTime.tryParse(isoValue);
  if (parsed == null) return isoValue;
  final local = parsed.toLocal();
  final date =
      '${local.year}-${local.month.toString().padLeft(2, '0')}-${local.day.toString().padLeft(2, '0')}';
  final time =
      '${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}:${local.second.toString().padLeft(2, '0')}';
  return '$date $time';
}

String _formatPeriod(String isoValue, {required bool monthOnly}) {
  final parsed = DateTime.tryParse(isoValue)?.toLocal();
  if (parsed == null) return isoValue;
  final month = parsed.month.toString().padLeft(2, '0');
  if (monthOnly) return '${parsed.year}-$month';
  final day = parsed.day.toString().padLeft(2, '0');
  return '${parsed.year}-$month-$day';
}

String _requestTypeLabel(CsnSupportRequestType? requestType) {
  return requestType == CsnSupportRequestType.liveChat
      ? 'Live Chat'
      : 'Video Call';
}

String _customerName(String? userName, String userId) {
  final cleanName = userName?.trim();
  if (cleanName != null && cleanName.isNotEmpty) return cleanName;
  return userId;
}

String _customerSubtitle(String? userEmail, String userId) {
  final cleanEmail = userEmail?.trim();
  if (cleanEmail != null && cleanEmail.isNotEmpty) return cleanEmail;
  return 'ID: $userId';
}

int _statsTotalForPeriod(
  List<StaffStatsItem> items, {
  required String period,
  required CsnSupportRequestType requestType,
}) {
  return items
      .where((item) =>
          _formatPeriod(item.periodStart, monthOnly: false) == period &&
          item.requestType == requestType)
      .fold<int>(0, (total, item) => total + item.total);
}

@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  WidgetsFlutterBinding.ensureInitialized();
  try {
    await Firebase.initializeApp();
  } catch (_) {
    // no-op
  }
  final plugin = FlutterLocalNotificationsPlugin();
  const settings = InitializationSettings(
    android: AndroidInitializationSettings(
        '@drawable/ic_admin_support_notification'),
  );
  await plugin.initialize(settings);
  await _showIncomingRequestNotificationFromMessage(plugin, message);
}

Future<void> _showIncomingRequestNotificationFromMessage(
  FlutterLocalNotificationsPlugin notifications,
  RemoteMessage message,
) async {
  final data = message.data;
  if (data['type'] != 'incoming_call_request') return;
  final requestId = (data['requestId'] ?? '').toString();
  final userId = (data['userId'] ?? 'User').toString();
  final userName = (data['userName'] ?? '').toString();
  final userEmail = (data['userEmail'] ?? '').toString();
  final id = requestId.isEmpty
      ? DateTime.now().millisecondsSinceEpoch
      : requestId.hashCode;
  const details = NotificationDetails(
    android: AndroidNotificationDetails(
      'incoming_calls',
      'Incoming Calls',
      channelDescription: 'Incoming queue request alerts',
      importance: Importance.max,
      priority: Priority.high,
      fullScreenIntent: true,
      category: AndroidNotificationCategory.call,
      icon: 'ic_admin_support_notification',
    ),
  );
  await notifications.show(
    id,
    'Incoming call request',
    '${_customerName(userName, userId)} (${_customerSubtitle(userEmail, userId)}) is waiting',
    details,
  );
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  try {
    await Firebase.initializeApp();
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  } catch (error, stackTrace) {
    debugLog('Firebase initialize failed', error, stackTrace);
  }
  runApp(const CsnExampleApp());
}

class CsnExampleApp extends StatelessWidget {
  const CsnExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _appName,
      debugShowCheckedModeBanner: false,
      theme: csnTheme(
        brightness: Brightness.dark,
        override: CsnThemeData.dark().copyWith(
          primary: const Color(0xFF22C3EE),
          accent: const Color(0xFF64D2FF),
          background: const Color(0xFF080D17),
          surface: const Color(0xFF111827),
          text: const Color(0xFFE6EDF7),
          mutedText: const Color(0xFF9FB0C9),
        ),
      ),
      home: const CsnHomePage(),
    );
  }
}

class CsnHomePage extends StatefulWidget {
  const CsnHomePage({super.key});

  @override
  State<CsnHomePage> createState() => _CsnHomePageState();
}

class _CsnHomePageState extends State<CsnHomePage> {
  static const String _baseUrl = 'https://api.deafott.com/';
  static const String _wsUrl = 'wss://api.deafott.com/ws';

  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  _StaffRole _selectedRole = _StaffRole.executive;
  String? _loginError;
  bool _authenticated = false;
  bool _sessionReady = false;
  bool _loadingLogin = false;
  bool _loadingAdminData = false;
  bool _changingAdminPassword = false;

  final _notifications = FlutterLocalNotificationsPlugin();
  final Set<String> _seenQueueIds = <String>{};
  final FirebaseMessaging _messaging = FirebaseMessaging.instance;

  CsnAdminRequestController? _adminController;
  String? _adminJwt;
  String? _currentUserId;
  String? _currentUserName;
  String? _currentUserEmail;
  _StaffRole? _currentRole;
  List<AuthUserProfile> _executives = [];
  List<StaffHistoryItem> _history = [];
  List<StaffHistoryItem> _executiveHistory = [];
  AdminStatsResponse? _adminStats;
  String? _activeRequestId;
  String? _fcmToken;
  bool _joiningCall = false;
  bool _connectingAdmin = false;
  bool _permissionsRequested = false;
  bool _firebaseReady = false;
  StreamSubscription<RemoteMessage>? _onMessageSub;
  StreamSubscription<String>? _onTokenRefreshSub;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      await _restoreSession();
      await _initNotifications();
      await _initFirebaseMessaging();
      await _requestMediaPermissionsOnce();
      if (mounted) {
        setState(() {
          _sessionReady = true;
        });
      }
    });
  }

  Future<void> _initNotifications() async {
    const settings = InitializationSettings(
      android: AndroidInitializationSettings(
          '@drawable/ic_admin_support_notification'),
    );
    await _notifications.initialize(settings);
    final android = _notifications.resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>();
    await android?.requestNotificationsPermission();
    const channel = AndroidNotificationChannel(
      'incoming_calls',
      'Incoming Calls',
      description: 'Incoming queue request alerts',
      importance: Importance.max,
    );
    await android?.createNotificationChannel(channel);
  }

  Future<void> _initFirebaseMessaging() async {
    try {
      await Firebase.initializeApp();
      _firebaseReady = true;
    } catch (error, stackTrace) {
      debugLog('Firebase init unavailable', error, stackTrace);
      _firebaseReady = false;
      return;
    }

    await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );
    _fcmToken = await _messaging.getToken();
    if (_fcmToken != null && _fcmToken!.isNotEmpty) {
      await _registerAdminPushTokenIfPossible(_fcmToken!);
    }

    _onTokenRefreshSub = _messaging.onTokenRefresh.listen((token) async {
      _fcmToken = token;
      await _registerAdminPushTokenIfPossible(token);
    });

    _onMessageSub = FirebaseMessaging.onMessage.listen((message) async {
      await _showIncomingRequestNotificationFromMessage(
          _notifications, message);
    });
  }

  Future<void> _requestMediaPermissionsOnce() async {
    if (_permissionsRequested) return;
    _permissionsRequested = true;
    try {
      final stream = await navigator.mediaDevices.getUserMedia({
        'audio': true,
        'video': true,
      });
      for (final track in stream.getTracks()) {
        await track.stop();
      }
      await stream.dispose();
    } catch (error, stackTrace) {
      debugLog('Media permission request failed', error, stackTrace);
    }
  }

  @override
  void dispose() {
    unawaited(_onMessageSub?.cancel());
    unawaited(_onTokenRefreshSub?.cancel());
    final adminJwt = (_adminJwt ?? '').trim();
    final token = _fcmToken;
    if (_currentRole == _StaffRole.executive &&
        adminJwt.isNotEmpty &&
        token != null &&
        token.isNotEmpty) {
      final api = CsnApiClient(baseUrl: _baseUrl, jwt: adminJwt);
      unawaited(api.unregisterAdminPushToken(token).whenComplete(api.close));
    }
    _emailController.dispose();
    _passwordController.dispose();
    _adminController?.dispose();
    super.dispose();
  }

  Future<void> _handleLogin() async {
    if (_loadingLogin) return;
    final email = _emailController.text.trim();
    final password = _passwordController.text;
    if (email.isEmpty || password.isEmpty) {
      setState(() {
        _loginError = 'Email and password are required';
      });
      return;
    }
    _loadingLogin = true;
    setState(() {});
    try {
      final client = CsnApiClient(baseUrl: _baseUrl);
      final login = _selectedRole == _StaffRole.admin
          ? await client.loginAdmin(email: email, password: password)
          : await client.loginExecutive(email: email, password: password);
      client.close();
      _adminJwt = login.token;
      _currentUserId = login.user.id;
      _currentUserName = login.user.name;
      _currentUserEmail = login.user.email;
      _currentRole = _selectedRole;
      await _saveSession();
      setState(() {
        _authenticated = true;
        _loginError = null;
      });
      if (_selectedRole == _StaffRole.executive) {
        await _connectExecutive();
      } else {
        await _loadAdminDashboard();
      }
    } catch (error, stackTrace) {
      debugLog('Staff login failed', error, stackTrace);
      setState(() {
        _loginError = 'Invalid email or password';
      });
    } finally {
      _loadingLogin = false;
      if (mounted) setState(() {});
    }
  }

  Future<void> _saveSession() async {
    final token = _adminJwt?.trim() ?? '';
    final role = _currentRole;
    final userId = _currentUserId?.trim() ?? '';
    if (token.isEmpty || role == null || userId.isEmpty) return;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_sessionTokenKey, token);
    await prefs.setString(_sessionRoleKey, role.name);
    await prefs.setString(_sessionUserIdKey, userId);
    await prefs.setString(_sessionUserNameKey, _currentUserName ?? '');
    await prefs.setString(_sessionUserEmailKey, _currentUserEmail ?? '');
  }

  Future<void> _clearSession() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_sessionTokenKey);
    await prefs.remove(_sessionRoleKey);
    await prefs.remove(_sessionUserIdKey);
    await prefs.remove(_sessionUserNameKey);
    await prefs.remove(_sessionUserEmailKey);
  }

  Future<void> _restoreSession() async {
    final prefs = await SharedPreferences.getInstance();
    final token = prefs.getString(_sessionTokenKey)?.trim() ?? '';
    final roleValue = prefs.getString(_sessionRoleKey)?.trim() ?? '';
    if (token.isEmpty || roleValue.isEmpty || _isJwtExpired(token)) {
      await _clearSession();
      return;
    }
    final role = _StaffRole.values.where((it) => it.name == roleValue).toList();
    if (role.isEmpty) {
      await _clearSession();
      return;
    }
    _adminJwt = token;
    _currentRole = role.first;
    _selectedRole = role.first;
    _currentUserId = prefs.getString(_sessionUserIdKey);
    _currentUserName = prefs.getString(_sessionUserNameKey);
    _currentUserEmail = prefs.getString(_sessionUserEmailKey);
    _authenticated = true;
    if (_currentRole == _StaffRole.executive) {
      await _connectExecutive();
    } else {
      await _loadAdminDashboard();
    }
  }

  bool _isJwtExpired(String token) {
    try {
      final parts = token.split('.');
      if (parts.length < 2) return true;
      final payload = parts[1];
      final normalized = base64Url.normalize(payload);
      final json = jsonDecode(utf8.decode(base64Url.decode(normalized)));
      if (json is! Map<String, dynamic>) return true;
      final exp = json['exp'];
      if (exp is! num) return true;
      final expiresAt = DateTime.fromMillisecondsSinceEpoch(exp.toInt() * 1000);
      return DateTime.now().isAfter(expiresAt);
    } catch (_) {
      return true;
    }
  }

  Future<void> _connectExecutive() async {
    if (_connectingAdmin) return;
    _connectingAdmin = true;
    setState(() {});
    try {
      if ((_adminJwt ?? '').isEmpty) {
        _showToast('Login required');
        return;
      }

      _adminController?.dispose();
      _adminController = CsnAdminRequestController(
        baseUrl: _baseUrl,
        adminJwt: _adminJwt!,
        wsUrl: _normalizeWsUrl(_wsUrl),
      );
      _adminController!.addListener(_onAdminUpdate);
      await _registerAdminPushTokenIfPossible(_fcmToken);
      await _adminController!.refreshQueue();
      final api = CsnApiClient(baseUrl: _baseUrl, jwt: _adminJwt);
      _executiveHistory = await api.getExecutiveHistory(limit: 150);
      api.close();
    } catch (error, stackTrace) {
      debugLog('Connect executive failed', error, stackTrace);
      final message = error.toString();
      if (message.contains('401')) {
        _showToast('Session expired. Please login again.');
        _logout();
      } else {
        _showToast('Failed to connect executive');
      }
    } finally {
      _connectingAdmin = false;
      if (mounted) setState(() {});
    }
  }

  Future<void> _loadAdminDashboard() async {
    final jwt = (_adminJwt ?? '').trim();
    if (jwt.isEmpty) return;
    _loadingAdminData = true;
    setState(() {});
    try {
      final api = CsnApiClient(baseUrl: _baseUrl, jwt: jwt);
      final executives = await api.listExecutives();
      final history = await api.getAdminHistory(limit: 300);
      final stats = await api.getAdminStats();
      api.close();
      _executives = executives;
      _history = history;
      _adminStats = stats;
    } catch (error, stackTrace) {
      debugLog('Load admin dashboard failed', error, stackTrace);
      final message = error.toString();
      if (message.contains('401')) {
        _showToast('Session expired. Please login again.');
        _logout();
      } else {
        _showToast('Failed to load admin dashboard');
      }
    } finally {
      _loadingAdminData = false;
      if (mounted) setState(() {});
    }
  }

  Future<void> _openExecutiveManagementPage() async {
    final jwt = (_adminJwt ?? '').trim();
    if (jwt.isEmpty) return;
    await Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => ExecutiveManagementPage(
          baseUrl: _baseUrl,
          jwt: jwt,
        ),
      ),
    );
    await _loadAdminDashboard();
  }

  Future<void> _changeOwnAdminPassword() async {
    if (_changingAdminPassword) return;
    final jwt = (_adminJwt ?? '').trim();
    if (jwt.isEmpty) return;
    final values = await _showChangePasswordDialog();
    if (values == null) return;
    final currentPassword = values.$1.trim();
    final newPassword = values.$2.trim();
    if (currentPassword.isEmpty || newPassword.isEmpty) {
      _showToast('Current and new password are required');
      return;
    }
    _changingAdminPassword = true;
    if (mounted) setState(() {});
    try {
      final api = CsnApiClient(baseUrl: _baseUrl, jwt: jwt);
      await api.changeAdminPassword(
        currentPassword: currentPassword,
        newPassword: newPassword,
      );
      api.close();
      _showToast('Password changed successfully');
    } catch (error, stackTrace) {
      debugLog('Change admin password failed', error, stackTrace);
      _showToast('Failed to change password');
    } finally {
      _changingAdminPassword = false;
      if (mounted) setState(() {});
    }
  }

  Future<(String, String)?> _showChangePasswordDialog() async {
    var currentPassword = '';
    var newPassword = '';
    final result = await showDialog<(String, String)>(
      context: context,
      builder: (dialogContext) => AlertDialog(
        title: const Text('Change My Password'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Current password',
              ),
              onChanged: (value) => currentPassword = value,
            ),
            const SizedBox(height: 10),
            TextField(
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'New password',
              ),
              onChanged: (value) => newPassword = value,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(dialogContext).pop(),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.of(dialogContext).pop(
              (currentPassword, newPassword),
            ),
            child: const Text('Change'),
          ),
        ],
      ),
    );
    return result;
  }

  Future<void> _onAdminUpdate() async {
    final controller = _adminController;
    if (controller == null) return;
    for (final item in controller.queue) {
      if (_seenQueueIds.add(item.requestId)) {
        await _showIncomingRequestNotification(item);
      }
    }
    if (mounted) {
      setState(() {});
    }
  }

  Future<void> _showIncomingRequestNotification(AdminQueueItem item) async {
    const details = NotificationDetails(
      android: AndroidNotificationDetails(
        'incoming_calls',
        'Incoming Calls',
        channelDescription: 'Incoming queue request alerts',
        importance: Importance.max,
        priority: Priority.high,
        fullScreenIntent: true,
        category: AndroidNotificationCategory.call,
        icon: 'ic_admin_support_notification',
      ),
    );
    await _notifications.show(
      item.requestId.hashCode,
      'Incoming call request',
      '${_customerName(item.userName, item.userId)} (${_customerSubtitle(item.userEmail, item.userId)}) is waiting',
      details,
    );
  }

  Future<void> _registerAdminPushTokenIfPossible(String? token) async {
    if (!_firebaseReady) return;
    if (_currentRole != _StaffRole.executive) return;
    final adminJwt = (_adminJwt ?? '').trim();
    final cleanToken = token?.trim() ?? '';
    if (adminJwt.isEmpty || cleanToken.isEmpty) return;
    try {
      final api = CsnApiClient(baseUrl: _baseUrl, jwt: adminJwt);
      await api.registerAdminPushToken(cleanToken);
      api.close();
      debugLog('Admin FCM token registered');
    } catch (error, stackTrace) {
      debugLog('Admin FCM token register failed', error, stackTrace);
    }
  }

  Future<void> _accept(AdminQueueItem item) async {
    final admin = _adminController;
    if (admin == null) return;
    final resolvedRequestType = await _resolveRequestType(item);
    final roomId = await admin.accept(item.requestId);
    if (roomId == null) return;
    _activeRequestId = item.requestId;
    Future<void> onEnded() async {
      if (_activeRequestId != null) {
        await admin.end(_activeRequestId!);
        _activeRequestId = null;
        final jwt = (_adminJwt ?? '').trim();
        if (jwt.isNotEmpty) {
          final api = CsnApiClient(baseUrl: _baseUrl, jwt: jwt);
          _executiveHistory = await api.getExecutiveHistory(limit: 150);
          api.close();
        }
      }
    }

    if (resolvedRequestType == CsnSupportRequestType.liveChat) {
      await _openLiveChat(
        jwt: (_adminJwt ?? '').trim(),
        roomId: roomId,
        requestId: item.requestId,
        localUserId: _currentUserId ?? 'executive',
        onEnded: onEnded,
      );
      return;
    }
    await _openCall(
      jwt: (_adminJwt ?? '').trim(),
      roomId: roomId,
      localUserId: _currentUserId ?? 'executive',
      onEnded: onEnded,
    );
  }

  Future<CsnSupportRequestType?> _resolveRequestType(
      AdminQueueItem item) async {
    if (item.requestType != null) return item.requestType;
    final jwt = (_adminJwt ?? '').trim();
    if (jwt.isEmpty) return null;
    try {
      final api = CsnApiClient(baseUrl: _baseUrl, jwt: jwt);
      final status = await api.getCallRequestStatus(item.requestId);
      api.close();
      return status.requestType;
    } catch (error, stackTrace) {
      debugLog('Resolve request type failed', error, stackTrace);
      return null;
    }
  }

  Future<void> _openCall({
    required String jwt,
    required String roomId,
    required String localUserId,
    required Future<void> Function() onEnded,
  }) async {
    if (_joiningCall) return;
    _joiningCall = true;
    try {
      await _requestMediaPermissionsOnce();
      final sdk = CsnSdk(
        baseUrl: _baseUrl,
        wsUrl: _normalizeWsUrl(_wsUrl),
        jwt: jwt,
      );
      final controller = CsnBasicCallController(
        apiClient: sdk.api,
        signalingClient: sdk.signaling(),
        localUserId: localUserId,
      );
      await controller.initialize();
      await controller.join(roomId);
      if (!mounted) {
        await controller.leave();
        controller.dispose();
        return;
      }
      await Navigator.of(context).push(
        MaterialPageRoute<void>(
          builder: (_) => CsnCallScreen(
            controller: controller,
            onEndCall: () {
              unawaited(onEnded());
            },
          ),
        ),
      );
      controller.dispose();
    } catch (error, stackTrace) {
      debugLog('Open call failed', error, stackTrace);
      _showToast('Failed to start call');
    } finally {
      _joiningCall = false;
    }
  }

  Future<void> _openLiveChat({
    required String jwt,
    required String roomId,
    required String requestId,
    required String localUserId,
    required Future<void> Function() onEnded,
  }) async {
    if (_joiningCall) return;
    _joiningCall = true;
    try {
      final sdk = CsnSdk(
        baseUrl: _baseUrl,
        wsUrl: _normalizeWsUrl(_wsUrl),
        jwt: jwt,
      );
      final controller = CsnLiveChatController(
        signalingClient: sdk.signaling(),
        localUserId: localUserId,
        roomId: roomId,
        requestId: requestId,
      );
      await Navigator.of(context).push(
        MaterialPageRoute<void>(
          builder: (_) => CsnLiveChatScreen(
            controller: controller,
            title: 'Support Chat',
            onEndChat: onEnded,
          ),
        ),
      );
      controller.dispose();
    } catch (error, stackTrace) {
      debugLog('Open live chat failed', error, stackTrace);
      _showToast('Failed to start live chat');
    } finally {
      _joiningCall = false;
    }
  }

  void _showToast(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(message)));
  }

  String _normalizeWsUrl(String raw) {
    final trimmed = raw.trim();
    if (trimmed.startsWith('ws://') || trimmed.startsWith('wss://')) {
      return trimmed;
    }
    if (trimmed.startsWith('http://')) {
      return trimmed.replaceFirst('http://', 'ws://');
    }
    if (trimmed.startsWith('https://')) {
      return trimmed.replaceFirst('https://', 'wss://');
    }
    return trimmed;
  }

  Widget _buildLoginPage(CsnThemeData theme) {
    return Scaffold(
      backgroundColor: theme.background,
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Color(0xFF050914), Color(0xFF111827), Color(0xFF0C162B)],
          ),
        ),
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(20),
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 420),
              child: Container(
                padding: const EdgeInsets.all(20),
                decoration: BoxDecoration(
                  color: const Color(0xAA0F172A),
                  borderRadius: BorderRadius.circular(20),
                  border: Border.all(color: const Color(0xFF2E3E5B)),
                  boxShadow: const [
                    BoxShadow(
                      color: Color(0x55000000),
                      blurRadius: 22,
                      offset: Offset(0, 12),
                    ),
                  ],
                ),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const _SupportLogo(size: 80),
                    const SizedBox(height: 14),
                    Text(
                      _appName,
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        color: theme.text,
                        fontSize: 24,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    const SizedBox(height: 6),
                    Text(
                      'Staff Login',
                      style: TextStyle(color: theme.mutedText),
                    ),
                    const SizedBox(height: 12),
                    SegmentedButton<_StaffRole>(
                      segments: const [
                        ButtonSegment<_StaffRole>(
                          value: _StaffRole.executive,
                          icon: Icon(Icons.support_agent),
                          label: Text('Executive'),
                        ),
                        ButtonSegment<_StaffRole>(
                          value: _StaffRole.admin,
                          icon: Icon(Icons.admin_panel_settings),
                          label: Text('Admin'),
                        ),
                      ],
                      selected: {_selectedRole},
                      onSelectionChanged: (value) {
                        setState(() {
                          _selectedRole = value.first;
                        });
                      },
                    ),
                    const SizedBox(height: 18),
                    TextField(
                      controller: _emailController,
                      style: TextStyle(color: theme.text),
                      decoration: const InputDecoration(
                        labelText: 'Email',
                        prefixIcon: Icon(Icons.email_outlined),
                      ),
                    ),
                    const SizedBox(height: 12),
                    TextField(
                      controller: _passwordController,
                      obscureText: true,
                      style: TextStyle(color: theme.text),
                      decoration: const InputDecoration(
                        labelText: 'Password',
                        prefixIcon: Icon(Icons.lock_outline),
                      ),
                      onSubmitted: (_) => unawaited(_handleLogin()),
                    ),
                    if (_loginError != null) ...[
                      const SizedBox(height: 10),
                      Text(
                        _loginError!,
                        style: const TextStyle(color: Color(0xFFF87171)),
                      ),
                    ],
                    const SizedBox(height: 16),
                    SizedBox(
                      width: double.infinity,
                      child: ElevatedButton.icon(
                        onPressed: _loadingLogin
                            ? null
                            : () => unawaited(_handleLogin()),
                        icon: const Icon(Icons.login),
                        label: Text(_loadingLogin ? 'Logging in...' : 'Login'),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  void _logout() {
    unawaited(_clearSession());
    _adminController?.dispose();
    _adminController = null;
    setState(() {
      _authenticated = false;
      _loginError = null;
      _passwordController.clear();
      _adminJwt = null;
      _currentUserId = null;
      _currentUserName = null;
      _currentUserEmail = null;
      _currentRole = null;
      _executives = [];
      _history = [];
      _executiveHistory = [];
      _adminStats = null;
    });
  }

  Widget _buildExecutivePage(CsnThemeData theme) {
    final admin = _adminController;
    return Scaffold(
      appBar: AppBar(
        backgroundColor: theme.surface,
        titleSpacing: 12,
        title: const Row(
          children: [
            _SupportLogo(size: 34),
            SizedBox(width: 10),
            Expanded(child: Text(_appName, overflow: TextOverflow.ellipsis)),
          ],
        ),
        actions: [
          IconButton(
            tooltip: 'Logout',
            onPressed: _logout,
            icon: const Icon(Icons.logout),
          ),
        ],
      ),
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Color(0xFF080D17), Color(0xFF0F172A)],
          ),
        ),
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            Container(
              padding: const EdgeInsets.all(14),
              decoration: BoxDecoration(
                color: const Color(0xCC111827),
                borderRadius: BorderRadius.circular(14),
                border: Border.all(color: const Color(0xFF2A3A52)),
              ),
              child: Column(
                children: [
                  Wrap(
                    spacing: 10,
                    runSpacing: 10,
                    children: [
                      ElevatedButton.icon(
                        onPressed: _connectingAdmin ? null : _connectExecutive,
                        icon: const Icon(Icons.support_agent),
                        label: Text(
                          _connectingAdmin
                              ? 'Connecting...'
                              : 'Connect Executive',
                        ),
                      ),
                      OutlinedButton.icon(
                        onPressed: admin?.refreshQueue,
                        icon: const Icon(Icons.refresh),
                        label: const Text('Refresh Queue'),
                      ),
                    ],
                  ),
                  const SizedBox(height: 12),
                  _InfoRow(label: 'Executive', value: _currentUserName ?? '-'),
                  _InfoRow(
                    label: 'Live',
                    value:
                        admin?.connected == true ? 'Connected' : 'Disconnected',
                  ),
                  _InfoRow(
                    label: 'Active Calls',
                    value: admin?.activeCount.toString() ?? '-',
                  ),
                  _InfoRow(
                    label: 'Avg Call Time',
                    value: admin == null
                        ? '-'
                        : _formatDurationSeconds(admin.averageCallSeconds),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 14),
            for (final item in admin?.queue ?? const <AdminQueueItem>[])
              Card(
                color: const Color(0xFF141C2E),
                child: ListTile(
                  title: Text(
                    _customerName(item.userName, item.userId),
                    style: const TextStyle(fontWeight: FontWeight.w600),
                  ),
                  subtitle: Text(
                    '${_customerSubtitle(item.userEmail, item.userId)}'
                    ' | ${_requestTypeLabel(item.requestType)}'
                    ' | Pos ${item.position} | ETA ${_formatDurationSeconds(item.etaSeconds)}'
                    ' | Requested ${_formatExactTime(item.createdAt)}',
                  ),
                  trailing: Wrap(
                    spacing: 8,
                    children: [
                      TextButton(
                        onPressed: () => _accept(item),
                        child: const Text('Accept'),
                      ),
                      TextButton(
                        onPressed: () => admin?.decline(item.requestId),
                        child: const Text('Decline'),
                      ),
                    ],
                  ),
                ),
              ),
            const SizedBox(height: 16),
            const Text(
              'My History',
              style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
            ),
            const SizedBox(height: 8),
            for (final item in _executiveHistory)
              Card(
                color: const Color(0xFF141C2E),
                child: ListTile(
                  title: Text(_customerName(item.userName, item.userId)),
                  subtitle: Text(
                    '${_customerSubtitle(item.userEmail, item.userId)}'
                    ' | ${_requestTypeLabel(item.requestType)} | Status: ${item.status}'
                    ' | Time: ${_formatDurationSeconds(item.timeTakenSeconds)}'
                    ' | Started: ${_formatExactTime(item.acceptedAt ?? item.createdAt)}'
                    ' | Ended: ${_formatExactTime(item.endedAt)}',
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatsSection(String title, List<StaffStatsItem> items,
      {required bool monthOnly, bool showExecutive = false}) {
    final visibleItems = items.take(12).toList();
    return Card(
      color: const Color(0xFF141C2E),
      child: Padding(
        padding: const EdgeInsets.all(14),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
            ),
            const SizedBox(height: 8),
            if (visibleItems.isEmpty)
              const Text('No records yet')
            else
              for (final item in visibleItems)
                _InfoRow(
                  label: showExecutive
                      ? '${item.executiveName ?? 'Unassigned'} | ${_formatPeriod(item.periodStart, monthOnly: monthOnly)}'
                      : _formatPeriod(item.periodStart, monthOnly: monthOnly),
                  value:
                      '${_requestTypeLabel(item.requestType)}: ${item.total}',
                ),
          ],
        ),
      ),
    );
  }

  Widget _buildAdminPage(CsnThemeData theme) {
    final stats = _adminStats;
    final today =
        _formatPeriod(DateTime.now().toIso8601String(), monthOnly: false);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: theme.surface,
        title: const Text('Admin Dashboard'),
        actions: [
          IconButton(
            tooltip: 'Change Password',
            onPressed: _changingAdminPassword
                ? null
                : () => unawaited(_changeOwnAdminPassword()),
            icon: const Icon(Icons.password),
          ),
          IconButton(
            tooltip: 'Refresh',
            onPressed: _loadingAdminData ? null : _loadAdminDashboard,
            icon: const Icon(Icons.refresh),
          ),
          IconButton(
            tooltip: 'Logout',
            onPressed: _logout,
            icon: const Icon(Icons.logout),
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Container(
            padding: const EdgeInsets.all(14),
            decoration: BoxDecoration(
              color: const Color(0xCC111827),
              borderRadius: BorderRadius.circular(14),
              border: Border.all(color: const Color(0xFF2A3A52)),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _InfoRow(label: 'Admin', value: _currentUserName ?? '-'),
                _InfoRow(label: 'Email', value: _currentUserEmail ?? '-'),
                _InfoRow(
                  label: 'Executives',
                  value: _executives.length.toString(),
                ),
                _InfoRow(
                  label: 'History Items',
                  value: _history.length.toString(),
                ),
                if (stats != null) ...[
                  _InfoRow(
                    label: 'Calls Today',
                    value: _statsTotalForPeriod(
                      stats.daily,
                      period: today,
                      requestType: CsnSupportRequestType.videoCall,
                    ).toString(),
                  ),
                  _InfoRow(
                    label: 'Chats Today',
                    value: _statsTotalForPeriod(
                      stats.daily,
                      period: today,
                      requestType: CsnSupportRequestType.liveChat,
                    ).toString(),
                  ),
                  _InfoRow(
                    label: 'Records',
                    value: 'Last ${stats.retentionDays} days',
                  ),
                ],
              ],
            ),
          ),
          const SizedBox(height: 16),
          if (stats != null) ...[
            _buildStatsSection(
              'Daily Call / Chat Records',
              stats.daily,
              monthOnly: false,
            ),
            const SizedBox(height: 12),
            _buildStatsSection(
              'Monthly Call / Chat Records',
              stats.monthly,
              monthOnly: true,
            ),
            const SizedBox(height: 12),
            _buildStatsSection(
              'Executive Daily Records',
              stats.executiveDaily,
              monthOnly: false,
              showExecutive: true,
            ),
            const SizedBox(height: 12),
            _buildStatsSection(
              'Executive Monthly Records',
              stats.executiveMonthly,
              monthOnly: true,
              showExecutive: true,
            ),
            const SizedBox(height: 16),
          ],
          Card(
            color: const Color(0xFF141C2E),
            child: Padding(
              padding: const EdgeInsets.all(14),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Executive Management',
                    style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
                  ),
                  const SizedBox(height: 8),
                  const Text(
                    'Open dedicated page to add, activate/deactivate, reset password and delete executives.',
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton.icon(
                    onPressed: _openExecutiveManagementPage,
                    icon: const Icon(Icons.manage_accounts),
                    label: const Text('Manage Executives'),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          const Text(
            'Call / Chat History',
            style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
          ),
          const SizedBox(height: 8),
          if (_loadingAdminData) const LinearProgressIndicator(),
          for (final item in _history)
            Card(
              color: const Color(0xFF141C2E),
              child: ListTile(
                title: Text(
                  '${item.executiveName ?? 'Unassigned'} -> ${_customerName(item.userName, item.userId)}',
                ),
                subtitle: Text(
                  '${_customerSubtitle(item.userEmail, item.userId)}'
                  ' | ${_requestTypeLabel(item.requestType)} | Status: ${item.status}'
                  ' | Time: ${_formatDurationSeconds(item.timeTakenSeconds)}'
                  ' | Started: ${_formatExactTime(item.acceptedAt ?? item.createdAt)}'
                  ' | Ended: ${_formatExactTime(item.endedAt)}',
                ),
              ),
            ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = CsnTheme.of(context);
    if (!_sessionReady) {
      return Scaffold(
        backgroundColor: theme.background,
        body: const Center(child: CircularProgressIndicator()),
      );
    }
    if (!_authenticated) return _buildLoginPage(theme);
    if (_currentRole == _StaffRole.admin) return _buildAdminPage(theme);
    return _buildExecutivePage(theme);
  }
}

class ExecutiveManagementPage extends StatefulWidget {
  const ExecutiveManagementPage({
    super.key,
    required this.baseUrl,
    required this.jwt,
  });

  final String baseUrl;
  final String jwt;

  @override
  State<ExecutiveManagementPage> createState() =>
      _ExecutiveManagementPageState();
}

class _ExecutiveManagementPageState extends State<ExecutiveManagementPage> {
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final Set<String> _updatingExecutiveIds = <String>{};
  final Set<String> _deletingExecutiveIds = <String>{};
  final Set<String> _resettingExecutiveIds = <String>{};
  bool _loading = false;
  bool _creating = false;
  List<AuthUserProfile> _executives = [];

  @override
  void initState() {
    super.initState();
    unawaited(_loadExecutives());
  }

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _loadExecutives() async {
    _loading = true;
    if (mounted) setState(() {});
    try {
      final api = CsnApiClient(baseUrl: widget.baseUrl, jwt: widget.jwt);
      _executives = await api.listExecutives();
      api.close();
    } catch (error, stackTrace) {
      debugLog('Load executives failed', error, stackTrace);
      _showToast('Failed to load executives');
    } finally {
      _loading = false;
      if (mounted) setState(() {});
    }
  }

  Future<void> _createExecutive() async {
    if (_creating) return;
    final name = _nameController.text.trim();
    final email = _emailController.text.trim();
    final password = _passwordController.text;
    if (name.isEmpty || email.isEmpty || password.isEmpty) {
      _showToast('Name, email and password are required');
      return;
    }
    _creating = true;
    if (mounted) setState(() {});
    try {
      final api = CsnApiClient(baseUrl: widget.baseUrl, jwt: widget.jwt);
      await api.createExecutive(name: name, email: email, password: password);
      api.close();
      _nameController.clear();
      _emailController.clear();
      _passwordController.clear();
      await _loadExecutives();
      _showToast('Executive created');
    } catch (error, stackTrace) {
      debugLog('Create executive failed', error, stackTrace);
      _showToast('Failed to create executive');
    } finally {
      _creating = false;
      if (mounted) setState(() {});
    }
  }

  Future<void> _toggleExecutiveActive(AuthUserProfile executive) async {
    if (_updatingExecutiveIds.contains(executive.id)) return;
    _updatingExecutiveIds.add(executive.id);
    if (mounted) setState(() {});
    try {
      final api = CsnApiClient(baseUrl: widget.baseUrl, jwt: widget.jwt);
      await api.updateExecutiveActive(
        executiveId: executive.id,
        isActive: !executive.isActive,
      );
      api.close();
      await _loadExecutives();
    } catch (error, stackTrace) {
      debugLog('Toggle executive active failed', error, stackTrace);
      _showToast('Failed to update executive status');
    } finally {
      _updatingExecutiveIds.remove(executive.id);
      if (mounted) setState(() {});
    }
  }

  Future<void> _resetExecutivePassword(AuthUserProfile executive) async {
    if (_resettingExecutiveIds.contains(executive.id)) return;
    final password = await _showPasswordPrompt(
      title: 'Reset Password',
      hint: 'New password for ${executive.name}',
      actionLabel: 'Reset',
    );
    if (password == null || password.trim().isEmpty) return;
    _resettingExecutiveIds.add(executive.id);
    if (mounted) setState(() {});
    try {
      final api = CsnApiClient(baseUrl: widget.baseUrl, jwt: widget.jwt);
      await api.resetExecutivePassword(
        executiveId: executive.id,
        password: password.trim(),
      );
      api.close();
      _showToast('Password reset for ${executive.name}');
    } catch (error, stackTrace) {
      debugLog('Reset executive password failed', error, stackTrace);
      _showToast('Failed to reset password');
    } finally {
      _resettingExecutiveIds.remove(executive.id);
      if (mounted) setState(() {});
    }
  }

  Future<void> _deleteExecutive(AuthUserProfile executive) async {
    if (_deletingExecutiveIds.contains(executive.id)) return;
    final confirmed = await _showConfirmDialog(
      title: 'Delete Executive',
      message:
          'Delete ${executive.name}? This removes executive login access immediately.',
      actionLabel: 'Delete',
    );
    if (!confirmed) return;
    _deletingExecutiveIds.add(executive.id);
    if (mounted) setState(() {});
    try {
      final api = CsnApiClient(baseUrl: widget.baseUrl, jwt: widget.jwt);
      await api.deleteExecutive(executiveId: executive.id);
      api.close();
      await _loadExecutives();
      _showToast('Executive deleted');
    } catch (error, stackTrace) {
      debugLog('Delete executive failed', error, stackTrace);
      _showToast('Failed to delete executive');
    } finally {
      _deletingExecutiveIds.remove(executive.id);
      if (mounted) setState(() {});
    }
  }

  Future<String?> _showPasswordPrompt({
    required String title,
    required String hint,
    required String actionLabel,
  }) async {
    var value = '';
    final result = await showDialog<String>(
      context: context,
      builder: (dialogContext) => AlertDialog(
        title: Text(title),
        content: TextField(
          autofocus: true,
          obscureText: true,
          decoration: InputDecoration(hintText: hint),
          onChanged: (text) => value = text,
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(dialogContext).pop(),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.of(dialogContext).pop(value),
            child: Text(actionLabel),
          ),
        ],
      ),
    );
    return result;
  }

  Future<bool> _showConfirmDialog({
    required String title,
    required String message,
    required String actionLabel,
  }) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: Text(actionLabel),
          ),
        ],
      ),
    );
    return result == true;
  }

  void _showToast(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(message)));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Manage Executives'),
        actions: [
          IconButton(
            tooltip: 'Refresh',
            onPressed: _loading ? null : _loadExecutives,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Card(
            color: const Color(0xFF141C2E),
            child: Padding(
              padding: const EdgeInsets.all(14),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text(
                    'Add Executive',
                    style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
                  ),
                  const SizedBox(height: 10),
                  TextField(
                    controller: _nameController,
                    decoration: const InputDecoration(labelText: 'Name'),
                  ),
                  const SizedBox(height: 10),
                  TextField(
                    controller: _emailController,
                    decoration: const InputDecoration(labelText: 'Email'),
                  ),
                  const SizedBox(height: 10),
                  TextField(
                    controller: _passwordController,
                    obscureText: true,
                    decoration: const InputDecoration(labelText: 'Password'),
                  ),
                  const SizedBox(height: 10),
                  ElevatedButton.icon(
                    onPressed:
                        _creating ? null : () => unawaited(_createExecutive()),
                    icon: const Icon(Icons.person_add_alt_1),
                    label: Text(_creating ? 'Creating...' : 'Create Executive'),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          const Text(
            'Executives',
            style: TextStyle(fontWeight: FontWeight.w700, fontSize: 16),
          ),
          const SizedBox(height: 8),
          if (_loading) const LinearProgressIndicator(),
          for (final executive in _executives)
            Card(
              color: const Color(0xFF141C2E),
              margin: const EdgeInsets.only(bottom: 10),
              child: Padding(
                padding:
                    const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      executive.name,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    const SizedBox(height: 6),
                    Text(
                      executive.email,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    const SizedBox(height: 4),
                    Text(
                      executive.id,
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(color: Color(0xFF9FB0C9)),
                    ),
                    const SizedBox(height: 8),
                    Row(
                      children: [
                        Switch(
                          value: executive.isActive,
                          activeThumbColor: Colors.white,
                          activeTrackColor: Colors.green,
                          inactiveTrackColor: Colors.grey.shade700,
                          onChanged: _updatingExecutiveIds
                                  .contains(executive.id)
                              ? null
                              : (_) =>
                                  unawaited(_toggleExecutiveActive(executive)),
                        ),
                        IconButton(
                          tooltip: 'Reset password',
                          onPressed: _resettingExecutiveIds
                                  .contains(executive.id)
                              ? null
                              : () =>
                                  unawaited(_resetExecutivePassword(executive)),
                          icon: const Icon(Icons.password),
                        ),
                        IconButton(
                          tooltip: 'Delete executive',
                          onPressed: _deletingExecutiveIds
                                  .contains(executive.id)
                              ? null
                              : () => unawaited(_deleteExecutive(executive)),
                          icon: const Icon(Icons.delete_outline),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
        ],
      ),
    );
  }
}

class _SupportLogo extends StatelessWidget {
  const _SupportLogo({required this.size});

  final double size;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      decoration: const BoxDecoration(
        shape: BoxShape.circle,
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Color(0xFF22C3EE), Color(0xFF1E5BFF)],
        ),
        boxShadow: [
          BoxShadow(
            color: Color(0x5522C3EE),
            blurRadius: 16,
            offset: Offset(0, 6),
          ),
        ],
      ),
      child: Icon(
        Icons.support_agent_rounded,
        color: Colors.white,
        size: size * 0.58,
      ),
    );
  }
}

class _InfoRow extends StatelessWidget {
  const _InfoRow({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Padding(
      padding: const EdgeInsets.only(bottom: 6),
      child: Row(
        children: [
          Expanded(
            child: Text(
              label,
              style: textTheme.bodyMedium?.copyWith(
                color: const Color(0xFF9FB0C9),
              ),
            ),
          ),
          Text(
            value,
            style: textTheme.bodyMedium?.copyWith(
              fontWeight: FontWeight.w600,
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
0
points
224
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for CSN realtime audio/video calling with prebuilt request-queue and call UI.

Repository (GitHub)
View/report issues

Topics

#webrtc #video-calling #signaling #realtime

License

unknown (license)

Dependencies

flutter, flutter_webrtc, http, url_launcher, web_socket_channel

More

Packages that depend on csn_flutter