diff --git a/CHANGELOG.md b/CHANGELOG.md index ca47628d..d34c5ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## Next Release +* BREAKING CHANGES: + * Feat: Completed migration to Federated Plugin structure. This requires one change: + ```dart + /// old + TwilioVoice.instance + + // new + TwilioVoicePlatform.instance + ``` * Feat: [Web] Add Twilio Device [DeviceState] accessor protecting un/registration. * Feat: [Web] Add Twilio Device `updateToken(String)` function to allow updating of active device tokens. * Fix: [Web] Twilio Device does not unregister on `unregister()` method call due to 'device.off' not visible in js object causing device event listeners to remain attached on unregistered device. diff --git a/README.md b/README.md index 8517e8e7..3da88647 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # twilio_voice -Provides an interface to Twilio's Programmable Voice SDK to allow voice-over-IP (VoIP) calling into -your Flutter applications. +Provides an interface to Twilio's Programmable Voice SDK to allow voice-over-IP (VoIP) calling into your Flutter applications. ~~This plugin was taken from the original `flutter_twilio_voice` as it seems that plugin is no longer maintained, this one is.~~ Project ownership & maintenance handed over by [diegogarcia](https://github.com/diegogarciar). For the foreseeable future, I'll be actively maintaining this project. #### 🐞Bug? Issue? Something odd? @@ -121,13 +120,13 @@ notifications: To register a Phone Account, request access to `READ_PHONE_NUMBERS` permission first: ```dart -TwilioVoice.instance.requestReadPhoneNumbersPermission(); // Gives Android permissions to read Phone Accounts +TwilioVoicePlatform.instance.requestReadPhoneNumbersPermission(); // Gives Android permissions to read Phone Accounts ``` then, register the `PhoneAccount` with: ```dart -TwilioVoice.instance.registerPhoneAccount(); +TwilioVoicePlatform.instance.registerPhoneAccount(); ``` #### Enable calling account @@ -135,13 +134,13 @@ TwilioVoice.instance.registerPhoneAccount(); To open the `Call Account` settings, use the following code: ```dart -TwilioVoice.instance.openPhoneAccountSettings(); +TwilioVoicePlatform.instance.openPhoneAccountSettings(); ``` Check if it's enabled with: ```dart -TwilioVoice.instance.isPhoneAccountEnabled(); +TwilioVoicePlatform.instance.isPhoneAccountEnabled(); ``` #### Calling with ConnectionService @@ -151,7 +150,7 @@ Placing a call with Telecom app via Connection Service requires a `PhoneAccount` Finally, to grant access to place calls, run: ```dart -TwilioVoice.instance.requestCallPhonePermission(); // Gives Android permissions to place calls +TwilioVoicePlatform.instance.requestCallPhonePermission(); // Gives Android permissions to place calls ``` See [Customizing the Calling Account](#customizing-the-calling-account) for more information. @@ -161,7 +160,7 @@ See [Customizing the Calling Account](#customizing-the-calling-account) for more To enable the `ConnectionService` and make/receive calls, run: ```dart -TwilioVoice.instance.requestReadPhoneStatePermission(); // Gives Android permissions to read Phone State +TwilioVoicePlatform.instance.requestReadPhoneStatePermission(); // Gives Android permissions to read Phone State ``` Highly recommended to review the notes for **Android**. See [[Notes]](https://github.com/cybex-dev/twilio_voice/blob/master/NOTES.md#android) for more information. @@ -272,7 +271,7 @@ import 'package:web_callkit/web_callkit.dart'; // Get call sid used as unique identifier void _notifyMissedCall() async { - final callSid = await TwilioVoice.instance.call.getSid(); + final callSid = await TwilioVoicePlatform.instance.call.getSid(); WebCallkit.instance.reportCallDisconnected(callSid!, response: CKDisconnectResponse.missed); } ``` @@ -308,25 +307,25 @@ for more information on preparing for publishing your macOS app ### Usage -The plugin was separated into two classes, the `TwilioVoice.instance` -and `TwilioVoice.instance.call`, the first one is in charge of general configuration and the second +The plugin was separated into two classes, the `TwilioVoicePlatform.instance` +and `TwilioVoicePlatform.instance.call`, the first one is in charge of general configuration and the second one is in charge of managing calls. Register iOS capabilities - Add Audio and Voice over IP in background modes -### TwilioVoice.instance +### TwilioVoicePlatform.instance #### Setting the tokens -call `TwilioVoice.instance.setTokens` as soon as your app starts. +call `TwilioVoicePlatform.instance.setTokens` as soon as your app starts. - `accessToken` provided from your server, you can see an example cloud function [here](https://github.com/cybex-dev/twilio_voice/blob/master/functions.js). - `deviceToken` is automatically handled on iOS, for android you need to pass a FCM token. -call `TwilioVoice.instance.unregister` to unregister from Twilio, if no access token is passed, it +call `TwilioVoicePlatform.instance.unregister` to unregister from Twilio, if no access token is passed, it will use the token provided in `setTokens` at the same session. ### Call Identifier @@ -339,13 +338,13 @@ register them so when they call, the call UI can display their names and not the #### Registering a client ``` -TwilioVoice.instance.registerClient(String clientId, String clientName) +TwilioVoicePlatform.instance.registerClient(String clientId, String clientName) ``` #### Unregistering a client ``` -TwilioVoice.instance.unregisterClient(String clientId) +TwilioVoicePlatform.instance.unregisterClient(String clientId) ``` #### Default caller @@ -354,12 +353,12 @@ You can also set a default caller, such as "unknown number" or "chat friend" in from an unregistered client. ``` -TwilioVoice.instance.setDefaultCallerName(String callerName) +TwilioVoicePlatform.instance.setDefaultCallerName(String callerName) ``` ### Call Events -use stream `TwilioVoice.instance.callEventsListener` to receive events from the TwilioSDK such as +use stream `TwilioVoicePlatform.instance.callEventsListener` to receive events from the TwilioSDK such as call events and logs, it is a broadcast so you can listen to it on different parts of your app. Some events might be missed when the app has not launched, please check out the example project to find the workarounds. @@ -449,7 +448,7 @@ to `false`. use `extraOptions` to pass additional variables to your server callback function. ``` - await TwilioVoice.instance.call.place(from:myId, to: clientId, extraOptions); + await TwilioVoicePlatform.instance.call.place(from:myId, to: clientId, extraOptions); ``` @@ -484,28 +483,28 @@ Receives calls via [ConnectionService](https://developer.android.com/reference/a #### Mute a Call ``` - TwilioVoice.instance.call.toggleMute(isMuted: true); + TwilioVoicePlatform.instance.call.toggleMute(isMuted: true); ``` #### Toggle Speaker ``` - TwilioVoice.instance.call.toggleSpeaker(speakerIsOn: true); + TwilioVoicePlatform.instance.call.toggleSpeaker(speakerIsOn: true); ``` #### Hang Up ``` - TwilioVoice.instance.call.hangUp(); + TwilioVoicePlatform.instance.call.hangUp(); ``` #### Send Digits ``` - TwilioVoice.instance.call.sendDigits(String digits); + TwilioVoicePlatform.instance.call.sendDigits(String digits); ``` @@ -516,16 +515,16 @@ Receives calls via [ConnectionService](https://developer.android.com/reference/a To receive and place calls you need Microphone permissions, register the microphone permission in your info.plist for iOS. -You can use `TwilioVoice.instance.hasMicAccess` and `TwilioVoice.instance.requestMicAccess` to check +You can use `TwilioVoicePlatform.instance.hasMicAccess` and `TwilioVoicePlatform.instance.requestMicAccess` to check and request the permission. Permissions is also automatically requested when receiving a call. #### Background calls (Android only on some devices) ~~Xiaomi devices, and maybe others, need a special permission to receive background calls. -use `TwilioVoice.instance.requiresBackgroundPermissions` to check if your device requires a special +use `TwilioVoicePlatform.instance.requiresBackgroundPermissions` to check if your device requires a special permission, if it does, show a rationale explaining the user why you need the permission. Finally call -`TwilioVoice.instance.requestBackgroundPermissions` which will take the user to the App Settings +`TwilioVoicePlatform.instance.requestBackgroundPermissions` which will take the user to the App Settings page to enable the permission.~~ Deprecated in 0.10.0, as it is no longer needed. Custom UI has been replaced with native UI. @@ -535,20 +534,20 @@ Deprecated in 0.10.0, as it is no longer needed. Custom UI has been replaced wit Similar to CallKit on iOS, Android implements their own via a [ConnectionService](https://developer.android.com/reference/android/telecom/ConnectionService) integration. To make use of this, you'll need to request `CALL_PHONE` permissions via: ```dart -TwilioVoice.instance.requestCallPhonePermission(); // Gives Android permissions to place outgoing calls -TwilioVoice.instance.requestReadPhoneStatePermission(); // Gives Android permissions to read Phone State including receiving calls -TwilioVoice.instance.requestReadPhoneNumbersPermission(); // Gives Android permissions to read Phone Accounts -TwilioVoice.instance.requestManageOwnCallsPermission(); // Gives Android permissions to manage calls, this isn't necessary to request as the permission is simply required in the Manifest, but added nontheless. +TwilioVoicePlatform.instance.requestCallPhonePermission(); // Gives Android permissions to place outgoing calls +TwilioVoicePlatform.instance.requestReadPhoneStatePermission(); // Gives Android permissions to read Phone State including receiving calls +TwilioVoicePlatform.instance.requestReadPhoneNumbersPermission(); // Gives Android permissions to read Phone Accounts +TwilioVoicePlatform.instance.requestManageOwnCallsPermission(); // Gives Android permissions to manage calls, this isn't necessary to request as the permission is simply required in the Manifest, but added nontheless. ``` Following this, to register a Phone Account (required by all applications implementing a system-managed `ConnectionService`, run: ```dart -TwilioVoice.instance.registerPhoneAccount(); // Registers the Phone Account -TwilioVoice.instance.openPhoneAccountSettings(); // Opens the Phone Account settings +TwilioVoicePlatform.instance.registerPhoneAccount(); // Registers the Phone Account +TwilioVoicePlatform.instance.openPhoneAccountSettings(); // Opens the Phone Account settings // After the account is enabled, you can check if it's enabled with: -TwilioVoice.instance.isPhoneAccountEnabled(); // Checks if the Phone Account is enabled +TwilioVoicePlatform.instance.isPhoneAccountEnabled(); // Checks if the Phone Account is enabled ``` This last step can be considered the 'final check' to make/receive calls on Android. @@ -558,8 +557,8 @@ This last step can be considered the 'final check' to make/receive calls on Andr Finally, a consideration for not all (`CALL_PHONE`) permissions granted on an Android device. The following feature is available on Android only: ```dart -TwilioVoice.instance.rejectCallOnNoPermissions({Bool = false}); // Rejects incoming calls if permissions are not granted -TwilioVoice.instance.isRejectingCallOnNoPermissions(); // Checks if the plugin is rejecting calls if permissions are not granted +TwilioVoicePlatform.instance.rejectCallOnNoPermissions({Bool = false}); // Rejects incoming calls if permissions are not granted +TwilioVoicePlatform.instance.isRejectingCallOnNoPermissions(); // Checks if the plugin is rejecting calls if permissions are not granted ``` If the `CALL_PHONE` permissions group i.e. `READ_PHONE_STATE`, `READ_PHONE_NUMBERS`, `CALL_PHONE` aren't granted nor a Phone Account is registered and enabled, the plugin will either reject the incoming call (true) or not show the incoming call UI (false). diff --git a/example/lib/dialogs/update_token_dialog.dart b/example/lib/dialogs/update_token_dialog.dart index e5b43f74..47786251 100644 --- a/example/lib/dialogs/update_token_dialog.dart +++ b/example/lib/dialogs/update_token_dialog.dart @@ -7,7 +7,7 @@ class UpdateTokenDialogContent extends StatelessWidget { Widget build(BuildContext context) { final textController = TextEditingController(); return AlertDialog( - title: Text('Paste your new token'), + title: const Text('Paste your new token'), content: SingleChildScrollView( child: ListBody( children: [ diff --git a/example/lib/main.dart b/example/lib/main.dart index 3c9a0c1c..b1ce9eac 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -153,7 +153,7 @@ class _AppState extends State { androidToken = await FirebaseMessaging.instance.getToken(); printDebug("androidToken is ${androidToken!}"); } - final result = await TwilioVoice.instance.setTokens(accessToken: accessToken, deviceToken: androidToken); + final result = await TwilioVoicePlatform.instance.setTokens(accessToken: accessToken, deviceToken: androidToken); return result ?? false; } @@ -263,7 +263,7 @@ class _AppState extends State { void initState() { super.initState(); - TwilioVoice.instance.setOnDeviceTokenChanged((token) { + TwilioVoicePlatform.instance.setOnDeviceTokenChanged((token) { printDebug("voip-device token changed"); if (!kIsWeb) { register(); @@ -274,29 +274,29 @@ class _AppState extends State { register(); const partnerId = "alicesId"; - TwilioVoice.instance.registerClient(partnerId, "Alice"); - // TwilioVoice.instance.requestReadPhoneStatePermission(); - // TwilioVoice.instance.requestMicAccess(); - // TwilioVoice.instance.requestCallPhonePermission(); + TwilioVoicePlatform.instance.registerClient(partnerId, "Alice"); + // TwilioVoicePlatform.instance.requestReadPhoneStatePermission(); + // TwilioVoicePlatform.instance.requestMicAccess(); + // TwilioVoicePlatform.instance.requestCallPhonePermission(); } /// Listen for call events void listenForEvents() { - TwilioVoice.instance.callEventsListener.listen((event) { + TwilioVoicePlatform.instance.callEventsListener.listen((event) { printDebug("voip-onCallStateChanged $event"); switch (event) { case CallEvent.incoming: // applies to web only if (kIsWeb || Platform.isAndroid) { - final activeCall = TwilioVoice.instance.call.activeCall; + final activeCall = TwilioVoicePlatform.instance.call.activeCall; if (activeCall != null && activeCall.callDirection == CallDirection.incoming) { _showWebIncomingCallDialog(); } } break; case CallEvent.ringing: - final activeCall = TwilioVoice.instance.call.activeCall; + final activeCall = TwilioVoicePlatform.instance.call.activeCall; if (activeCall != null) { final customData = activeCall.customParams; if (customData != null) { @@ -324,13 +324,13 @@ class _AppState extends State { /// Place a call to [clientIdentifier] Future _onPerformCall(String clientIdentifier) async { - if (!await (TwilioVoice.instance.hasMicAccess())) { + if (!await (TwilioVoicePlatform.instance.hasMicAccess())) { printDebug("request mic access"); - TwilioVoice.instance.requestMicAccess(); + TwilioVoicePlatform.instance.requestMicAccess(); return; } printDebug("starting call to $clientIdentifier"); - TwilioVoice.instance.call.place(to: clientIdentifier, from: userId, extraOptions: {"_TWI_SUBJECT": "Company Name"}); + TwilioVoicePlatform.instance.call.place(to: clientIdentifier, from: userId, extraOptions: {"_TWI_SUBJECT": "Company Name"}); } Future _onRegisterWithToken(String token, [String? identity]) async { @@ -402,14 +402,14 @@ class _AppState extends State { /// Show incoming call dialog for web and Android void _showWebIncomingCallDialog() async { showingIncomingCallDialog = true; - final activeCall = TwilioVoice.instance.call.activeCall!; + final activeCall = TwilioVoicePlatform.instance.call.activeCall!; final action = await showIncomingCallScreen(context, activeCall); if (action == true) { printDebug("accepting call"); - TwilioVoice.instance.call.answer(); + TwilioVoicePlatform.instance.call.answer(); } else if (action == false) { printDebug("rejecting call"); - TwilioVoice.instance.call.hangUp(); + TwilioVoicePlatform.instance.call.hangUp(); } else { printDebug("no action"); } @@ -458,7 +458,7 @@ class _LogoutAction extends StatelessWidget { Widget build(BuildContext context) { return TextButton.icon( onPressed: () async { - final result = await TwilioVoice.instance.unregister(); + final result = await TwilioVoicePlatform.instance.unregister(); if (result == true) { onSuccess?.call(); } else { @@ -471,7 +471,7 @@ class _LogoutAction extends StatelessWidget { } class _UpdateTokenAction extends StatelessWidget { - const _UpdateTokenAction({super.key}); + const _UpdateTokenAction({Key? key}): super(key: key); @override Widget build(BuildContext context) { @@ -485,8 +485,9 @@ class _UpdateTokenAction extends StatelessWidget { if (token?.isEmpty ?? true) { return; } - final result = await TwilioVoice.instance.setTokens(accessToken: token!); + final result = await TwilioVoicePlatform.instance.setTokens(accessToken: token!); final message = (result ?? false) ? "Successfully updated token" : "Failed to update token"; + // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message)), ); diff --git a/example/lib/screens/ui_call_screen.dart b/example/lib/screens/ui_call_screen.dart index c28918f7..c603fcb8 100644 --- a/example/lib/screens/ui_call_screen.dart +++ b/example/lib/screens/ui_call_screen.dart @@ -135,7 +135,7 @@ class _RingSound extends StatefulWidget { } class _RingSoundState extends State<_RingSound> { - final _tv = TwilioVoice.instance; + final _tv = TwilioVoicePlatform.instance; final TextEditingController _controller = TextEditingController(); @override diff --git a/example/lib/screens/widgets/call_features.dart b/example/lib/screens/widgets/call_features.dart index b3c8989f..f8ceb422 100644 --- a/example/lib/screens/widgets/call_features.dart +++ b/example/lib/screens/widgets/call_features.dart @@ -53,7 +53,7 @@ class _CallControlsState extends State { //#endregion - final _tv = TwilioVoice.instance; + final _tv = TwilioVoicePlatform.instance; bool activeCall = false; @override diff --git a/example/lib/screens/widgets/call_status.dart b/example/lib/screens/widgets/call_status.dart index 43c95dc1..1ca7c7ea 100644 --- a/example/lib/screens/widgets/call_status.dart +++ b/example/lib/screens/widgets/call_status.dart @@ -78,13 +78,13 @@ class _CallStatusState extends State { @override Widget build(BuildContext context) { return StreamBuilder( - stream: TwilioVoice.instance.callEventsListener, + stream: TwilioVoicePlatform.instance.callEventsListener, builder: (context, snapshot) { _addEvent(snapshot.data); return FutureBuilder( - future: TwilioVoice.instance.call.isOnCall(), + future: TwilioVoicePlatform.instance.call.isOnCall(), builder: (context, snapshot) { - final activeCall = TwilioVoice.instance.call.activeCall; + final activeCall = TwilioVoicePlatform.instance.call.activeCall; return Column( children: [ _buildOnCallStatus(onCall: snapshot.data == true), @@ -106,7 +106,7 @@ class _CallSID extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder( - future: TwilioVoice.instance.call.getSid(), + future: TwilioVoicePlatform.instance.call.getSid(), builder: (context, snapshot) { final sid = snapshot.data ?? "N/A"; return Text(sid); diff --git a/example/lib/screens/widgets/permissions_block.dart b/example/lib/screens/widgets/permissions_block.dart index 8d827e3f..d14734a8 100644 --- a/example/lib/screens/widgets/permissions_block.dart +++ b/example/lib/screens/widgets/permissions_block.dart @@ -19,7 +19,7 @@ class PermissionsBlock extends StatefulWidget { class _PermissionsBlockState extends State with WidgetsBindingObserver { AppLifecycleState? _lastLifecycleState; - final _tv = TwilioVoice.instance; + final _tv = TwilioVoicePlatform.instance; bool activeCall = false; //#region #region Permissions diff --git a/example/lib/screens/widgets/twilio_log.dart b/example/lib/screens/widgets/twilio_log.dart index f5aa007d..1e850ef0 100644 --- a/example/lib/screens/widgets/twilio_log.dart +++ b/example/lib/screens/widgets/twilio_log.dart @@ -12,7 +12,7 @@ class TwilioLog extends StatefulWidget { class _TwilioLogState extends State { late final StreamSubscription _subscription; - final _tv = TwilioVoice.instance; + final _tv = TwilioVoicePlatform.instance; final _events = []; @override diff --git a/lib/_internal/method_channel/twilio_voice_method_channel.dart b/lib/_internal/method_channel/twilio_voice_method_channel.dart index ecbbcf4f..37fc981c 100644 --- a/lib/_internal/method_channel/twilio_voice_method_channel.dart +++ b/lib/_internal/method_channel/twilio_voice_method_channel.dart @@ -12,8 +12,6 @@ import 'twilio_call_method_channel.dart'; /// Implementation of [TwilioVoicePlatform] that uses method channels. class MethodChannelTwilioVoice extends TwilioVoicePlatform { - static TwilioVoicePlatform get instance => TwilioVoicePlatform.instance; - late final TwilioCallPlatform _call = MethodChannelTwilioCall(); @override diff --git a/lib/_internal/twilio_voice_web.dart b/lib/_internal/twilio_voice_web.dart index 9aea66d6..09191270 100644 --- a/lib/_internal/twilio_voice_web.dart +++ b/lib/_internal/twilio_voice_web.dart @@ -23,7 +23,6 @@ import 'package:twilio_voice/_internal/js/call/call_status.dart'; // TODO(cybex-dev) implement js_interop for js_util package // ignore: unused_import,deprecated_member_use import 'package:js/js_util.dart'; -import 'package:twilio_voice/_internal/platform_interface/twilio_voice_platform_interface.dart'; import 'package:web_callkit/web_callkit_web.dart'; import '../twilio_voice.dart'; diff --git a/lib/twilio_voice.dart b/lib/twilio_voice.dart index 6341f96b..9b1906ec 100644 --- a/lib/twilio_voice.dart +++ b/lib/twilio_voice.dart @@ -3,13 +3,13 @@ library twilio_voice; import 'package:twilio_voice/_internal/method_channel/twilio_call_method_channel.dart'; import '_internal/method_channel/twilio_voice_method_channel.dart'; -import '_internal/platform_interface/twilio_voice_platform_interface.dart'; +export '_internal/platform_interface/twilio_voice_platform_interface.dart' show TwilioVoicePlatform; export './models/active_call.dart'; export './models/call_event.dart'; class TwilioVoice extends MethodChannelTwilioVoice { - static TwilioVoicePlatform get instance => MethodChannelTwilioVoice.instance; + } class Call extends MethodChannelTwilioCall {}