All checks were successful
Create Release / build (push) Successful in 3s
- Voice token: use AccessToken + VoiceGrant instead of browser-only ClientToken - Agent status: delegate to TWP_Agent_Manager matching browser phone behavior - Queue loading: add missing require_once for TWP_User_Queue_Manager - Add /phone-numbers endpoint for caller ID selection - Webhook: support CallerId param from mobile extraOptions - Flutter: caller ID dropdown in dialer, error logging in all catch blocks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
2.7 KiB
Dart
104 lines
2.7 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:twilio_voice/twilio_voice.dart';
|
|
import 'api_client.dart';
|
|
|
|
class VoiceService {
|
|
final ApiClient _api;
|
|
Timer? _tokenRefreshTimer;
|
|
String? _identity;
|
|
|
|
final StreamController<CallEvent> _callEventController =
|
|
StreamController<CallEvent>.broadcast();
|
|
Stream<CallEvent> get callEvents => _callEventController.stream;
|
|
|
|
VoiceService(this._api);
|
|
|
|
Future<void> initialize() async {
|
|
await _fetchAndRegisterToken();
|
|
|
|
TwilioVoice.instance.callEventsListener.listen((event) {
|
|
_callEventController.add(event);
|
|
});
|
|
|
|
// Refresh token every 50 minutes
|
|
_tokenRefreshTimer?.cancel();
|
|
_tokenRefreshTimer = Timer.periodic(
|
|
const Duration(minutes: 50),
|
|
(_) => _fetchAndRegisterToken(),
|
|
);
|
|
}
|
|
|
|
Future<void> _fetchAndRegisterToken() async {
|
|
try {
|
|
final response = await _api.dio.get('/voice/token');
|
|
final data = response.data;
|
|
final token = data['token'] as String;
|
|
_identity = data['identity'] as String;
|
|
await TwilioVoice.instance.setTokens(accessToken: token);
|
|
} catch (e) {
|
|
debugPrint('VoiceService._fetchAndRegisterToken error: $e');
|
|
}
|
|
}
|
|
|
|
String? get identity => _identity;
|
|
|
|
Future<void> answer() async {
|
|
await TwilioVoice.instance.call.answer();
|
|
}
|
|
|
|
Future<void> reject() async {
|
|
await TwilioVoice.instance.call.hangUp();
|
|
}
|
|
|
|
Future<void> hangUp() async {
|
|
await TwilioVoice.instance.call.hangUp();
|
|
}
|
|
|
|
Future<void> toggleMute(bool mute) async {
|
|
await TwilioVoice.instance.call.toggleMute(mute);
|
|
}
|
|
|
|
Future<void> toggleSpeaker(bool speaker) async {
|
|
await TwilioVoice.instance.call.toggleSpeaker(speaker);
|
|
}
|
|
|
|
Future<bool> makeCall(String to, {String? callerId}) async {
|
|
try {
|
|
final extraOptions = <String, dynamic>{};
|
|
if (callerId != null && callerId.isNotEmpty) {
|
|
extraOptions['CallerId'] = callerId;
|
|
}
|
|
return await TwilioVoice.instance.call.place(
|
|
to: to,
|
|
from: _identity ?? '',
|
|
extraOptions: extraOptions,
|
|
) ?? false;
|
|
} catch (e) {
|
|
debugPrint('VoiceService.makeCall error: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> sendDigits(String digits) async {
|
|
await TwilioVoice.instance.call.sendDigits(digits);
|
|
}
|
|
|
|
Future<void> holdCall(String callSid) async {
|
|
await _api.dio.post('/calls/$callSid/hold');
|
|
}
|
|
|
|
Future<void> unholdCall(String callSid) async {
|
|
await _api.dio.post('/calls/$callSid/unhold');
|
|
}
|
|
|
|
Future<void> transferCall(String callSid, String target) async {
|
|
await _api.dio.post('/calls/$callSid/transfer', data: {'target': target});
|
|
}
|
|
|
|
void dispose() {
|
|
_tokenRefreshTimer?.cancel();
|
|
_callEventController.close();
|
|
}
|
|
}
|