// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'dart:collection';
import 'package:dartx/dartx.dart';
import 'package:fleasy/fleasy.dart';
import 'package:flutter/material.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stop_watch_timer/stop_watch_timer.dart';
import 'package:telnyx_webrtc/config/telnyx_config.dart';
import 'package:telnyx_webrtc/model/socket_method.dart';
import 'package:telnyx_webrtc/model/telnyx_message.dart';
import 'package:telnyx_webrtc/model/telnyx_socket_error.dart';
import 'package:telnyx_webrtc/model/verto/receive/received_message_body.dart';
import 'package:telnyx_webrtc/telnyx_client.dart';
import 'package:uuid/uuid.dart';
import '../../../di.dart';
import '../../domain/entities/credentials_response/credentials_response.dart';
import '../../domain/services/api_client.dart';
import '../repository/analytics_repository.dart';
part 'telnyx_provider.freezed.dart';
enum CallError {
lowBalance,
notConnected,
dialError,
onGoingCall,
}
class CallQueue {
CallQueue({
required this.destination,
required this.callerName,
required this.forceCall,
});
final String destination;
final String callerName;
final bool forceCall;
}
@freezed
class TelnyxState with _$TelnyxState {
const factory TelnyxState({
@Default(false) bool registered,
@Default(false) bool onGoingCall,
@Default(false) bool isMuted,
@Default(false) bool isSpeakerOn,
@Default(false) bool onGoingInvitation,
@Default(false) bool callStarted,
String? onGoingCallName,
String? onGoingCallNumber,
IncomingInviteParams? incomingInviteParams,
StopWatchTimer? stopwatch,
CredentialsResponse? credentialsResponse,
required Queue<CallQueue> callQueue,
}) = _TelnyxState;
}
extension TelnyxStateX on TelnyxState {
bool get isCalling => onGoingInvitation || onGoingCall;
}
final telnyxClientProvider = Provider<TelnyxClient>((ref) {
return TelnyxClient()..connect();
});
final telnyxProvider = StateNotifierProvider<TelnyxStateNotifier, TelnyxState>(
(ref) => TelnyxStateNotifier(ref.watch(telnyxClientProvider)),
);
class TelnyxStateNotifier extends StateNotifier<TelnyxState> {
TelnyxStateNotifier(this._telnyxClient) : super(TelnyxState(callQueue: Queue<CallQueue>()));
final TelnyxClient _telnyxClient;
// final AsyncValue<User?> _user;
// just a flag to avoid multiple registering
bool _isRegistering = false;
void checkConnected() {
if (!_telnyxClient.isConnected()) {
_telnyxClient.connect();
}
}
Future<void> login({bool forceLogin = false, bool credCheck = false}) async {
if (_isRegistering) {
return;
}
checkConnected();
if (!state.registered || forceLogin) {
_isRegistering = true;
final creds = await getCredentials(credCheck);
if (creds?.credentials != null) {
await 2.seconds.delay;
_credentialLogin(
creds!.credentials!.login!,
creds.credentials!.password!,
'Bigly Sales',
creds.numbers!.first,
);
observeResponses();
}
_isRegistering = false;
}
}
Future<CredentialsResponse?> getCredentials(bool dontForce) async {
final creds = await di<APIClient>().getCredentials();
if (creds.credentials == null && !dontForce) {
unawaited(di<APIClient>().getCredentials(force: true));
} else {
state = state.copyWith(credentialsResponse: creds);
}
return creds.credentials != null ? creds : null;
}
void _credentialLogin(String uname, String password, String? name, String number) {
_telnyxClient.credentialLogin(
CredentialConfig(
uname,
password,
name ?? '',
number,
null,
true,
),
);
}
@override
void dispose() {
state.stopwatch?.dispose();
super.dispose();
}
void observeResponses() {
logg.d('Observing Telnyx responses');
_telnyxClient
..onSocketMessageReceived = (TelnyxMessage message) async {
logg.d(message.message.toJson());
switch (message.socketMethod) {
case SocketMethod.CLIENT_READY:
{
print('logged in to telnyx');
state = state.copyWith(registered: true);
if (state.callQueue.isNotEmpty) {
final qCall = state.callQueue.removeFirst();
await call(
destination: qCall.destination,
callerName: qCall.callerName,
forceCall: qCall.forceCall,
);
}
break;
}
case SocketMethod.INVITE:
{
final incomingInvite = message.message.inviteParams;
logg.d('incoming call');
state = state.copyWith(
onGoingInvitation: true,
incomingInviteParams: incomingInvite,
onGoingCallName: incomingInvite?.callerIdName,
onGoingCallNumber: incomingInvite?.callerIdNumber,
);
await showCallNotification();
break;
}
case SocketMethod.ANSWER:
{
state = state.copyWith(onGoingCall: true);
break;
}
case SocketMethod.BYE:
{
state.stopwatch?.onEnded?.call();
state = state.copyWith(
onGoingCall: false,
onGoingInvitation: false,
incomingInviteParams: null,
stopwatch: null,
onGoingCallName: null,
onGoingCallNumber: null,
);
await FlutterCallkitIncoming.endAllCalls();
break;
}
case SocketMethod.RINGING:
{
// _outGoingInvitation = true;
break;
}
}
}
..onSocketErrorReceived = (TelnyxSocketError error) {
logg.e('${error.errorCode} : ${error.errorMessage}');
di<AnalyticsRepository>().logTelnyxError(
error.errorCode.toString(),
error.errorMessage,
);
switch (error.errorCode) {
case -32000:
{
login();
break;
}
case -32001:
{
//Todo handle credential error
break;
}
case -32003:
{
//Todo handle gateway timeout error
break;
}
case -32004:
{
//ToDo hande gateway failure error
break;
}
}
};
}
Future<void> call({
required String destination,
required String callerName,
bool forceCall = false,
}) async {
final status = await Permission.microphone.request();
state = state.copyWith(callStarted: true);
if (!state.registered) {
await login();
}
final credits = await di<APIClient>().getCredits();
if (state.credentialsResponse != null && (credits.creditBalance ?? 0) <= 500) {
if (!forceCall) {
state = state.copyWith(callStarted: false);
throw CallError.lowBalance;
}
}
// if (callerName == null || state.credentialsResponse?.credentials == null) {
// state = state.copyWith(callStarted: false);
// throw CallError.notConnected;
// }
if (status.isGranted && !state.onGoingCall) {
try {
final destinationNumber = destination.startsWith('+') ? destination : '+1$destination';
if (state.credentialsResponse == null) {
state = state.copyWith(
callQueue: state.callQueue
..add(
CallQueue(
destination: destination,
callerName: callerName,
forceCall: forceCall,
),
));
return;
}
_telnyxClient.createCall().newInvite(
callerName,
state.credentialsResponse!.numbers!.first,
destinationNumber,
'Fake State',
);
state = state.copyWith(
onGoingCallName: callerName,
onGoingCallNumber: destinationNumber,
onGoingCall: true,
stopwatch: StopWatchTimer()..onStartTimer(),
);
await di<AnalyticsRepository>().logCall(
callerNumber: state.credentialsResponse!.numbers!.first,
destination: destinationNumber,
name: callerName,
);
} catch (e) {
state = state.copyWith(onGoingCall: false, callStarted: false);
logg.e(e);
rethrow;
}
} else {
state = state.copyWith(callStarted: false);
throw CallError.onGoingCall;
}
disableSpeakerOnStream();
}
void accept() {
if (state.incomingInviteParams != null) {
_telnyxClient.createCall().acceptCall(
state.incomingInviteParams!,
state.incomingInviteParams!.callerIdName!,
state.incomingInviteParams!.callerIdNumber!,
'State',
);
state = state.copyWith(
onGoingCall: true,
callStarted: true,
onGoingInvitation: false,
incomingInviteParams: null,
stopwatch: StopWatchTimer()..onStartTimer(),
);
disableSpeakerOnStream();
} else {
throw ArgumentError(state.incomingInviteParams);
}
}
void endCall() {
FlutterCallkitIncoming.endAllCalls();
if (state.onGoingCall) {
_telnyxClient.call.endCall(_telnyxClient.call.callId);
} else {
// edge case
try {
_telnyxClient.createCall().endCall(state.incomingInviteParams?.callID);
if (_telnyxClient.call.sessionCallerName.isNotEmpty) {
_telnyxClient.call.endCall(_telnyxClient.call.callId);
}
} catch (e) {
logg.e(e);
}
}
state.stopwatch?.onEnded?.call();
state = state.copyWith(
onGoingInvitation: false,
incomingInviteParams: null,
callStarted: false,
onGoingCall: false,
stopwatch: null,
onGoingCallName: null,
onGoingCallNumber: null,
);
}
void holdUnhold() => _telnyxClient.call.onHoldUnholdPressed();
void toggleSpeaker() {
state = state.copyWith(isSpeakerOn: !state.isSpeakerOn);
_telnyxClient.call.enableSpeakerPhone(state.isSpeakerOn);
}
void disableSpeakerOnStream() {
_telnyxClient.call.peerConnection?.onLocalStream = (stream) {
stream.getAudioTracks().first.enableSpeakerphone(false);
};
state = state.copyWith(isSpeakerOn: false);
}
void muteUnmute() {
_telnyxClient.call.onMuteUnmutePressed();
state = state.copyWith(isMuted: !state.isMuted);
}
Future<void> showCallNotification() async {
final currentUuid = const Uuid().v4();
final params = {
'id': currentUuid,
'nameCaller': state.onGoingCallName ?? 'Unknown Caller',
'appName': 'Bigly Phone',
'handle': state.onGoingCallNumber ?? 'Unknown Number',
'type': 0,
'textAccept': 'Accept',
'textDecline': 'Decline',
'textMissedCall': 'Missed call',
'textCallback': 'Call back',
'duration': 30000,
'android': {
'isCustomNotification': true,
'isShowLogo': false,
'isShowCallback': false,
'isShowMissedCallNotification': true,
'ringtonePath': 'system_ringtone_default',
'backgroundColor': '#3CB777',
'actionColor': '#4CAF50'
},
'ios': {
'iconName': 'CallKitLogo',
'handleType': 'generic',
'supportsVideo': true,
'maximumCallGroups': 2,
'maximumCallsPerCallGroup': 1,
'audioSessionMode': 'default',
'audioSessionActive': true,
'audioSessionPreferredSampleRate': 44100.0,
'audioSessionPreferredIOBufferDuration': 0.005,
'supportsDTMF': true,
'supportsHolding': true,
'supportsGrouping': false,
'supportsUngrouping': false,
'ringtonePath': 'system_ringtone_default'
}
};
await FlutterCallkitIncoming.showCallkitIncoming(params);
}
}