// 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); } }