Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
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;
|
2026-03-07 17:11:02 -08:00
|
|
|
bool _pendingAutoAnswer = false;
|
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
|
|
|
|
|
CallInfo get callInfo => _callInfo;
|
|
|
|
|
|
|
|
|
|
CallProvider(this._voiceService) {
|
|
|
|
|
_eventSub = _voiceService.callEvents.listen(_handleCallEvent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleCallEvent(CallEvent event) {
|
|
|
|
|
switch (event) {
|
|
|
|
|
case CallEvent.incoming:
|
2026-03-07 17:11:02 -08:00
|
|
|
if (_pendingAutoAnswer) {
|
|
|
|
|
_pendingAutoAnswer = false;
|
|
|
|
|
_callInfo = _callInfo.copyWith(state: CallState.connecting);
|
|
|
|
|
_voiceService.answer();
|
|
|
|
|
} else {
|
|
|
|
|
_callInfo = _callInfo.copyWith(state: CallState.ringing);
|
|
|
|
|
}
|
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 17:11:02 -08:00
|
|
|
// Update caller info from active call (skip if call just ended)
|
|
|
|
|
if (_callInfo.state != CallState.idle) {
|
|
|
|
|
final call = TwilioVoice.instance.call;
|
|
|
|
|
final active = call.activeCall;
|
|
|
|
|
if (active != null) {
|
|
|
|
|
if (_callInfo.callerNumber == null) {
|
|
|
|
|
_callInfo = _callInfo.copyWith(
|
|
|
|
|
callerNumber: active.from,
|
|
|
|
|
);
|
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
}
|
2026-03-07 17:11:02 -08:00
|
|
|
// Fetch SID asynchronously
|
|
|
|
|
call.getSid().then((sid) {
|
|
|
|
|
if (sid != null && sid != _callInfo.callSid && _callInfo.isActive) {
|
|
|
|
|
_callInfo = _callInfo.copyWith(callSid: sid);
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2026-03-07 17:11:02 -08:00
|
|
|
Future<void> hangUp() async {
|
|
|
|
|
await _voiceService.hangUp();
|
|
|
|
|
// If SDK didn't fire callEnded (e.g. no active SDK call), reset manually
|
|
|
|
|
if (_callInfo.state != CallState.idle) {
|
|
|
|
|
_stopDurationTimer();
|
|
|
|
|
_callInfo = const CallInfo();
|
|
|
|
|
_pendingAutoAnswer = false;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
}
|
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-03-06 18:05:54 -08:00
|
|
|
Future<void> makeCall(String number, {String? callerId}) async {
|
2026-03-06 15:32:22 -08:00
|
|
|
_callInfo = _callInfo.copyWith(
|
|
|
|
|
state: CallState.connecting,
|
|
|
|
|
callerNumber: number,
|
|
|
|
|
);
|
|
|
|
|
notifyListeners();
|
2026-03-07 17:11:02 -08:00
|
|
|
final success = await _voiceService.makeCall(number, callerId: callerId);
|
|
|
|
|
if (!success) {
|
|
|
|
|
debugPrint('CallProvider.makeCall: call.place() returned false');
|
|
|
|
|
_callInfo = const CallInfo(); // reset to idle
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
2026-03-06 15:32:22 -08:00
|
|
|
}
|
|
|
|
|
|
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 17:11:02 -08:00
|
|
|
Future<void> acceptQueueCall(String callSid) async {
|
|
|
|
|
_pendingAutoAnswer = true;
|
|
|
|
|
_callInfo = _callInfo.copyWith(state: CallState.connecting);
|
|
|
|
|
notifyListeners();
|
|
|
|
|
try {
|
|
|
|
|
await _voiceService.acceptQueueCall(callSid);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('CallProvider.acceptQueueCall error: $e');
|
|
|
|
|
_pendingAutoAnswer = false;
|
|
|
|
|
_callInfo = const CallInfo();
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add TWP Softphone Flutter app and complete mobile backend API
Backend: Add /voice/token endpoint with AccessToken + VoiceGrant for
mobile VoIP, implement unhold_call() with call leg detection, wire FCM
push notifications into call queue and webhook missed call handlers,
add data-only FCM message support for Android background wake, and add
Twilio API Key / Push Credential settings fields.
Flutter app: Full softphone with Twilio Voice SDK integration, JWT auth
with auto-refresh, SSE real-time queue updates, FCM push notifications,
Material 3 UI with dashboard, active call screen, dialpad, and call
controls (mute/speaker/hold/transfer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:01:23 -08:00
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_stopDurationTimer();
|
|
|
|
|
_eventSub?.cancel();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|