cicare_rtc_flutter 1.0.2
cicare_rtc_flutter: ^1.0.2 copied to clipboard
A professional RTC SDK for Flutter providing App-to-App and SIP calling with WhatsApp-style UI and FCM integration.
example/lib/main.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:cicare_rtc_flutter/cicare_rtc_flutter.dart';
import 'screens/login_page.dart';
import 'screens/dashboard_page.dart';
import 'services/auth_service.dart';
import 'models/call_state.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final CicareRtcFlutter _sdk = CicareRtcFlutter();
// Global status for call tracking
final ValueNotifier<CallState> callStateNotifier = ValueNotifier(CallState());
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
await _handleIncomingCallMessage(message);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(const ExampleApp());
}
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
@override
void initState() {
super.initState();
_sdk.setAPI(
baseUrl: "END POINT",
token: "TOKEN",
);
_setupMessaging();
_setupRtcCallbacks();
}
void _setupRtcCallbacks() {
CicareRtcFlutter.setOnCallStateChanged((state) {
print('RTC: Call state changed: $state');
// Update global state while preserving current callee info if applicable
callStateNotifier.value = callStateNotifier.value.copyWith(
status: _mapStringToStatus(state),
);
});
CicareRtcFlutter.setOnCallError((code, message) {
print('RTC: Call Error: [$code] $message');
callStateNotifier.value = callStateNotifier.value.copyWith(
status: CallStatus.error,
errorMessage: message,
);
});
}
CallStatus _mapStringToStatus(String state) {
switch (state.toUpperCase()) {
case 'CALLING': return CallStatus.calling;
case 'RINGING': return CallStatus.ringing;
case 'CONNECTED': return CallStatus.connected;
case 'DISCONNECTED': return CallStatus.ended;
default: return CallStatus.idle;
}
}
Future<void> _setupMessaging() async {
try {
await FirebaseMessaging.instance.requestPermission();
FirebaseMessaging.instance.onTokenRefresh.listen((token) {
AuthService.syncFcmToken();
});
FirebaseMessaging.onMessage.listen((msg) async {
print('FCM: Foreground message: ${msg.data}');
await _handleIncomingCallMessage(msg);
});
FirebaseMessaging.onMessageOpenedApp.listen(_handleIncomingCallMessage);
} catch (e) {
print('FCM: Setup error: $e');
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
title: 'CiCare SDK Call',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
routes: {
'/': (c) => const _Startup(),
'/login': (c) => const LoginPage(),
'/dashboard': (c) => const DashboardPage(),
},
);
}
}
Future<void> _handleIncomingCallMessage(RemoteMessage message) async {
print("Incoming call");
final data = Map<String, dynamic>.from(message.data);
if (!_looksLikeIncomingCall(data)) {
return;
}
final currentUser = await AuthService.getUser();
final payload = _IncomingCallPayload.fromMessageData(data, currentUser?.id);
if (payload == null) {
print('FCM: Incoming call payload incomplete: $data');
return;
}
try {
await _sdk.showIncoming(
callerId: payload.callerId,
callerName: payload.callerName,
callerAvatar: payload.callerAvatar,
calleeId: payload.calleeId,
calleeName: payload.calleeName,
calleeAvatar: payload.calleeAvatar,
checkSum: payload.checkSum,
metaData: payload.metaData,
);
} catch (e) {
print('FCM: Failed to show incoming call: $e');
}
}
bool _looksLikeIncomingCall(Map<String, dynamic> data) {
final event = (data['event'] ?? data['type'] ?? data['call_type'] ?? '')
.toString()
.toLowerCase();
return event.contains('incoming') ||
event.contains('call') ||
data.containsKey('alert_data') ||
data.containsKey('callerId') ||
data.containsKey('caller_id');
}
class _IncomingCallPayload {
const _IncomingCallPayload({
required this.callerId,
required this.callerName,
required this.callerAvatar,
required this.calleeId,
required this.calleeName,
required this.calleeAvatar,
required this.checkSum,
required this.metaData,
});
final String callerId;
final String? callerName;
final String? callerAvatar;
final String calleeId;
final String? calleeName;
final String? calleeAvatar;
final String checkSum;
final Map<String, String> metaData;
static _IncomingCallPayload? fromMessageData(
Map<String, dynamic> data,
String? fallbackCalleeId,
) {
final metaData = _extractMetaData(data);
final callerId = _firstString(data, ['callerId', 'caller_id', 'from_user_id']);
final calleeId =
_firstString(data, ['calleeId', 'callee_id', 'user_id', 'to_user_id']) ??
fallbackCalleeId ?? "0";
final checkSum = _firstString(data, ['checkSum', 'checksum', 'check_sum']) ?? "";
if (callerId == null || !metaData.containsKey('alert_data')) {
return null;
}
return _IncomingCallPayload(
callerId: callerId,
callerName: _firstString(data, ['callerName', 'caller_name', 'from_user_name']),
callerAvatar: _firstString(data, ['callerAvatar', 'caller_avatar', 'from_user_avatar']),
calleeId: calleeId,
calleeName: _firstString(data, ['calleeName', 'callee_name', 'to_user_name']),
calleeAvatar: _firstString(data, ['calleeAvatar', 'callee_avatar', 'to_user_avatar']),
checkSum: checkSum,
metaData: metaData,
);
}
static Map<String, String> _extractMetaData(Map<String, dynamic> data) {
final result = <String, String>{};
final rawMeta = data['metaData'] ?? data['metadata'] ?? data['meta_data'];
if (rawMeta is Map) {
for (final entry in rawMeta.entries) {
result[entry.key.toString()] = entry.value.toString();
}
} else if (rawMeta is String && rawMeta.isNotEmpty) {
try {
final decoded = jsonDecode(rawMeta);
if (decoded is Map) {
for (final entry in decoded.entries) {
result[entry.key.toString()] = entry.value.toString();
}
}
} catch (_) {}
}
for (final entry in data.entries) {
if (entry.key.startsWith('meta_') && entry.value != null) {
result[entry.key.substring(5)] = entry.value.toString();
}
}
final alertData = _firstString(data, ['alert_data']);
if (alertData != null) {
result['alert_data'] = alertData;
}
return result;
}
static String? _firstString(Map<String, dynamic> data, List<String> keys) {
for (final key in keys) {
final value = data[key];
if (value == null) continue;
final text = value.toString().trim();
if (text.isNotEmpty) return text;
}
return null;
}
}
class _Startup extends StatefulWidget {
const _Startup();
@override
State<_Startup> createState() => _StartupState();
}
class _StartupState extends State<_Startup> {
@override
void initState() {
super.initState();
_checkLogin();
}
Future<void> _checkLogin() async {
final user = await AuthService.getUser();
if (!mounted) return;
if (user != null) {
Navigator.of(context).pushReplacementNamed('/dashboard');
} else {
Navigator.of(context).pushReplacementNamed('/login');
}
}
@override
Widget build(BuildContext context) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}