tccc_user_call_sdk 0.0.3
tccc_user_call_sdk: ^0.0.3 copied to clipboard
Tencent Cloud Contact Center (TCCC) Flutter SDK for user-side audio outbound calls, with optional in-app call UI and DTMF.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:tccc_user_call_sdk/tccc_user_call_sdk.dart';
import 'package:tccc_user_call_sdk/ui.dart';
import 'example_auth.dart';
/// Published example. Git checkout internal run:
/// `cd example && flutter run -t lib/dev/internal_main.dart`.
void main() {
bootstrapExample(publishedExampleAuth);
}
void bootstrapExample(ExampleAuth auth) {
WidgetsFlutterBinding.ensureInitialized();
TcccConfig.update(const TcccConfig(locale: Locale('en')));
final navigatorKey = GlobalKey<NavigatorState>();
TcccCallUI.install(navigatorKey: navigatorKey);
runApp(ExampleApp(navigatorKey: navigatorKey, auth: auth));
}
class ExampleApp extends StatelessWidget {
ExampleApp({
super.key,
required this.navigatorKey,
this.auth = publishedExampleAuth,
});
final GlobalKey<NavigatorState> navigatorKey;
final ExampleAuth auth;
/// Public SDK facade. Use [TcccUserCall.instance].
final TcccUserCall userCallInstance = TcccUserCall.instance;
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
title: 'tccc_user_call_sdk example',
home: OutboundPage(userCallInstance: userCallInstance, auth: auth),
);
}
}
class OutboundPage extends StatefulWidget {
const OutboundPage({
super.key,
required this.userCallInstance,
required this.auth,
});
final TcccUserCall userCallInstance;
final ExampleAuth auth;
@override
State<OutboundPage> createState() => _OutboundPageState();
}
class _OutboundPageState extends State<OutboundPage> {
late final TextEditingController _sdkAppId;
late final TextEditingController _userId;
late final TextEditingController _userSig;
late final TextEditingController _channelIdController;
late String _channelId;
String _status =
'Tap createUser, then init and wait for Ready, then startAudioCall. Uninit after all calls.';
bool _busy = false;
bool _userCreated = false;
bool _sdkReady = false;
bool _releaseEnvReady = false;
StreamSubscription<TcccEvent>? _events;
StreamSubscription<TcccEvent>? _sessionEvents;
TcccUserCall get userCallInstance => widget.userCallInstance;
ExampleAuth get auth => widget.auth;
bool get _idle => !_busy && userCallInstance.currentCall == null;
bool get _canCreateUser => _idle && !_userCreated;
bool get _canInit => _idle && _userCreated;
bool get _canStart =>
!_busy && _sdkReady && userCallInstance.currentCall == null;
bool get _canUninit => _idle && (_userCreated || _sdkReady);
@override
void initState() {
super.initState();
_sdkAppId = TextEditingController(text: auth.sdkAppId);
_userId = TextEditingController(text: auth.userId);
_userSig = TextEditingController();
_channelId = auth.channelIds.isEmpty ? '' : auth.channelIds.first;
_channelIdController = TextEditingController(text: _channelId);
unawaited(userCallInstance.setConsoleEnabled(true));
// 先订阅 ready,再 init
_events = userCallInstance.events.listen((event) {
switch (event.type) {
case 'ready':
debugPrint('[tccc_user_call_sdk example] init ready');
if (!mounted) {
return;
}
setState(() {
_sdkReady = true;
_status = 'init ready';
});
break;
case 'error':
final e = event as TcccErrorEvent;
debugPrint(
'[tccc_user_call_sdk example] error ${e.serverType} ${e.failure.code.toInt()} ${e.failure.message}',
);
break;
case 'connecting':
final e = event as TcccConnectingEvent;
debugPrint(
'[tccc_user_call_sdk example] connecting ${e.serverType} attempts=${e.attempts}',
);
break;
case 'connected':
final e = event as TcccConnectedEvent;
debugPrint('[tccc_user_call_sdk example] connected ${e.serverType}');
break;
case 'disconnected':
final e = event as TcccDisconnectedEvent;
debugPrint(
'[tccc_user_call_sdk example] disconnected ${e.serverType} error=${e.error} code=${e.code} reason=${e.reason}',
);
break;
default:
debugPrint('[tccc_user_call_sdk example] ${event.type}');
break;
}
});
}
@override
void dispose() {
_sessionEvents?.cancel();
_events?.cancel();
unawaited(userCallInstance.unInit());
_sdkAppId.dispose();
_userId.dispose();
_userSig.dispose();
_channelIdController.dispose();
super.dispose();
}
/// Android demo uses the production visitor API, not `setDebugMode`.
/// Clear a leftover debug override from a previous hot-reload session.
Future<void> _ensureReleaseEnv() async {
if (_releaseEnvReady) {
return;
}
final result =
await userCallInstance.callExperimentalAPI('setReleaseMode', '{}');
if (!result.success) {
throw StateError(
'setReleaseMode failed ${result.code.toInt()} ${result.message}',
);
}
_releaseEnvReady = true;
}
/// Binds identity only. Does not connect; tap init next.
Future<void> _createUser() async {
setState(() => _busy = true);
try {
await _ensureReleaseEnv();
final sdkAppId = int.tryParse(_sdkAppId.text.trim()) ?? 0;
final userId = _userId.text.trim();
if (sdkAppId <= 0) {
if (mounted) {
setState(() => _status = 'createUser failed: sdkAppId is empty');
}
return;
}
if (userId.isEmpty) {
if (mounted) {
setState(() => _status = 'createUser failed: userId is empty');
}
return;
}
final userSig = auth.fetchesUserSig
? await auth.fetchUserSig!(userId)
: _userSig.text.trim();
if (userSig.isEmpty) {
if (mounted) {
setState(() => _status = 'createUser failed: userSig is empty');
}
return;
}
final created = userCallInstance.createUser(
sdkAppId: sdkAppId,
userId: userId,
userSig: userSig,
userClientData: auth.clientData,
);
if (!mounted) {
return;
}
if (!created.success) {
if (created.code == ErrorCode.User.instanceExists) {
setState(() {
_userCreated = true;
_status =
'createUser ${created.code.toInt()}: identity already bound. Tap init, or unInit to change user.';
});
return;
}
setState(() {
_userCreated = false;
_status =
'createUser failed ${created.code.toInt()} ${created.message}';
});
return;
}
setState(() {
_userCreated = true;
_status = 'createUser ok; tap init';
});
} catch (error) {
if (mounted) {
setState(() => _status = 'createUser failed $error');
}
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
Future<void> _init() async {
setState(() => _busy = true);
try {
await _ensureReleaseEnv();
final status = await userCallInstance.init();
if (!mounted) {
return;
}
if (!status.success) {
setState(() {
_sdkReady = false;
_status = 'init failed ${status.code.toInt()} ${status.message}';
});
return;
}
setState(() {
_sdkReady = true;
if (!_status.startsWith('init ready')) {
_status = 'init succeeded';
}
});
} catch (error) {
if (mounted) {
setState(() => _status = 'init failed $error');
}
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
Future<void> _startAudioCall() async {
setState(() => _busy = true);
try {
final channelId =
auth.fetchesUserSig ? _channelId : _channelIdController.text.trim();
if (channelId.isEmpty) {
if (mounted) {
setState(() => _status = 'start failed: channelId is empty');
}
return;
}
final started = await userCallInstance.startAudioCall(
channelId,
showUI: true,
);
if (!started.success) {
if (mounted) {
setState(() {
_status = 'start failed ${started.code.toInt()} ${started.message}';
});
}
return;
}
final session = started.data!;
await _sessionEvents?.cancel();
_sessionEvents = session.events.listen((event) {
if (event.type == 'ended') {
debugPrint('[tccc_user_call_sdk example] session ended');
}
});
if (mounted) {
setState(() => _status = 'calling ${session.callId}');
}
final result = await session.ended;
if (mounted) {
final failure = result.failure;
setState(() {
_status = failure == null
? 'ended ${result.cause.name}'
: 'ended ${result.cause.name} ${failure.code.toInt()} ${failure.message}';
});
}
} catch (error) {
if (mounted) {
setState(() => _status = 'call failed $error');
}
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
Future<void> _unInit() async {
setState(() => _busy = true);
try {
final status = await userCallInstance.unInit();
if (!mounted) {
return;
}
if (!status.success) {
setState(() {
_status = 'unInit failed ${status.code.toInt()} ${status.message}';
});
return;
}
setState(() {
_userCreated = false;
_sdkReady = false;
_status = 'unInited';
});
} catch (error) {
if (mounted) {
setState(() => _status = 'unInit failed $error');
}
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('TCCC outbound')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(
controller: _sdkAppId,
enabled: _canCreateUser,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'sdkAppId'),
),
TextField(
controller: _userId,
enabled: _canCreateUser,
decoration: const InputDecoration(labelText: 'userId'),
),
if (!auth.fetchesUserSig)
TextField(
controller: _userSig,
enabled: _canCreateUser,
decoration: const InputDecoration(labelText: 'userSig'),
),
if (auth.fetchesUserSig)
DropdownButtonFormField<String>(
// Flutter 3.7 exposes `value`; `initialValue` exists only from 3.33.
// ignore: deprecated_member_use
value: _channelId,
isExpanded: true,
decoration: const InputDecoration(labelText: 'channelId'),
items: [
for (var i = 0; i < auth.channelIds.length; i++)
DropdownMenuItem(
value: auth.channelIds[i],
child: Text('Audio ${i + 1} ${auth.channelIds[i]}'),
),
],
onChanged: _busy
? null
: (value) {
if (value != null) {
setState(() => _channelId = value);
}
},
)
else
TextField(
controller: _channelIdController,
decoration: const InputDecoration(labelText: 'channelId'),
),
const SizedBox(height: 12),
Text(_status),
const SizedBox(height: 12),
FilledButton(
onPressed: _canCreateUser ? _createUser : null,
child: const Text('createUser'),
),
const SizedBox(height: 8),
FilledButton(
onPressed: _canInit ? _init : null,
child: const Text('init'),
),
const SizedBox(height: 8),
FilledButton(
onPressed: _canStart ? _startAudioCall : null,
child: const Text('startAudioCall'),
),
const SizedBox(height: 8),
OutlinedButton(
onPressed: _canUninit ? _unInit : null,
child: const Text('unInit'),
),
],
),
);
}
}