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>
144 lines
3.9 KiB
Dart
144 lines
3.9 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:twilio_voice/twilio_voice.dart';
|
|
import '../models/call_info.dart';
|
|
import '../services/voice_service.dart';
|
|
|
|
class CallProvider extends ChangeNotifier {
|
|
final VoiceService _voiceService;
|
|
CallInfo _callInfo = const CallInfo();
|
|
Timer? _durationTimer;
|
|
StreamSubscription? _eventSub;
|
|
DateTime? _connectedAt;
|
|
|
|
CallInfo get callInfo => _callInfo;
|
|
|
|
CallProvider(this._voiceService) {
|
|
_eventSub = _voiceService.callEvents.listen(_handleCallEvent);
|
|
}
|
|
|
|
void _handleCallEvent(CallEvent event) {
|
|
switch (event) {
|
|
case CallEvent.incoming:
|
|
_callInfo = _callInfo.copyWith(
|
|
state: CallState.ringing,
|
|
);
|
|
break;
|
|
case CallEvent.ringing:
|
|
_callInfo = _callInfo.copyWith(state: CallState.connecting);
|
|
break;
|
|
case CallEvent.connected:
|
|
_connectedAt = DateTime.now();
|
|
_callInfo = _callInfo.copyWith(state: CallState.connected);
|
|
_startDurationTimer();
|
|
break;
|
|
case CallEvent.callEnded:
|
|
_stopDurationTimer();
|
|
_callInfo = const CallInfo(); // reset to idle
|
|
break;
|
|
case CallEvent.returningCall:
|
|
_callInfo = _callInfo.copyWith(state: CallState.connecting);
|
|
break;
|
|
case CallEvent.reconnecting:
|
|
break;
|
|
case CallEvent.reconnected:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Update caller info from active call
|
|
final call = TwilioVoice.instance.call;
|
|
final active = call.activeCall;
|
|
if (active != null) {
|
|
_callInfo = _callInfo.copyWith(
|
|
callerNumber: active.from,
|
|
);
|
|
// Fetch SID asynchronously
|
|
call.getSid().then((sid) {
|
|
if (sid != null && sid != _callInfo.callSid) {
|
|
_callInfo = _callInfo.copyWith(callSid: sid);
|
|
notifyListeners();
|
|
}
|
|
});
|
|
}
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
void _startDurationTimer() {
|
|
_durationTimer?.cancel();
|
|
_durationTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
|
if (_connectedAt != null) {
|
|
_callInfo = _callInfo.copyWith(
|
|
duration: DateTime.now().difference(_connectedAt!),
|
|
);
|
|
notifyListeners();
|
|
}
|
|
});
|
|
}
|
|
|
|
void _stopDurationTimer() {
|
|
_durationTimer?.cancel();
|
|
_connectedAt = null;
|
|
}
|
|
|
|
Future<void> answer() => _voiceService.answer();
|
|
Future<void> reject() => _voiceService.reject();
|
|
Future<void> hangUp() => _voiceService.hangUp();
|
|
|
|
Future<void> toggleMute() async {
|
|
final newMuted = !_callInfo.isMuted;
|
|
await _voiceService.toggleMute(newMuted);
|
|
_callInfo = _callInfo.copyWith(isMuted: newMuted);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> toggleSpeaker() async {
|
|
final newSpeaker = !_callInfo.isSpeakerOn;
|
|
await _voiceService.toggleSpeaker(newSpeaker);
|
|
_callInfo = _callInfo.copyWith(isSpeakerOn: newSpeaker);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> sendDigits(String digits) => _voiceService.sendDigits(digits);
|
|
|
|
Future<void> makeCall(String number, {String? callerId}) async {
|
|
_callInfo = _callInfo.copyWith(
|
|
state: CallState.connecting,
|
|
callerNumber: number,
|
|
);
|
|
notifyListeners();
|
|
await _voiceService.makeCall(number, callerId: callerId);
|
|
}
|
|
|
|
Future<void> holdCall() async {
|
|
final sid = _callInfo.callSid;
|
|
if (sid == null) return;
|
|
await _voiceService.holdCall(sid);
|
|
_callInfo = _callInfo.copyWith(isOnHold: true);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> unholdCall() async {
|
|
final sid = _callInfo.callSid;
|
|
if (sid == null) return;
|
|
await _voiceService.unholdCall(sid);
|
|
_callInfo = _callInfo.copyWith(isOnHold: false);
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> transferCall(String target) async {
|
|
final sid = _callInfo.callSid;
|
|
if (sid == null) return;
|
|
await _voiceService.transferCall(sid, target);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_stopDurationTimer();
|
|
_eventSub?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|