diff --git a/CHANGELOG.md b/CHANGELOG.md index 90b0ba67..106823ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Next Release * 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. +* Feat: update example. * Docs: update CHANGELOG ## 0.3.2+2 diff --git a/example/lib/dialogs/update_token_dialog.dart b/example/lib/dialogs/update_token_dialog.dart new file mode 100644 index 00000000..e5b43f74 --- /dev/null +++ b/example/lib/dialogs/update_token_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class UpdateTokenDialogContent extends StatelessWidget { + const UpdateTokenDialogContent({super.key}); + + @override + Widget build(BuildContext context) { + final textController = TextEditingController(); + return AlertDialog( + title: Text('Paste your new token'), + content: SingleChildScrollView( + child: ListBody( + children: [ + const Text('Paste your new token to continue making calls, you\'ll still receive calls to the current device.'), + const SizedBox(height: 16), + TextField( + controller: textController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'New Token', + alignLabelWithHint: true, + ), + maxLines: 3, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(null); // User chose not to update the token + }, + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(textController.text); // User chose to update the token + }, + child: const Text('Update Token'), + ), + ], + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index c917e9d8..3c9a0c1c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,6 +8,7 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:twilio_voice/twilio_voice.dart'; +import 'package:twilio_voice_example/dialogs/update_token_dialog.dart'; import 'package:twilio_voice_example/screens/ui_call_screen.dart'; import 'package:twilio_voice_example/screens/ui_registration_screen.dart'; @@ -103,25 +104,25 @@ class _AppState extends State { // Use for locally provided token generator e.g. Twilio's quickstarter project: https://github.com/twilio/voice-quickstart-server-node // if (!kIsWeb) { - bool success = false; - // if not web, we use the requested registration method - switch (widget.registrationMethod) { - case RegistrationMethod.env: - success = await _registerFromEnvironment(); - break; - case RegistrationMethod.url: - success = await _registerUrl(); - break; - case RegistrationMethod.firebase: - success = await _registerFirebase(); - break; - } + bool success = false; + // if not web, we use the requested registration method + switch (widget.registrationMethod) { + case RegistrationMethod.env: + success = await _registerFromEnvironment(); + break; + case RegistrationMethod.url: + success = await _registerUrl(); + break; + case RegistrationMethod.firebase: + success = await _registerFirebase(); + break; + } - if (success) { - setState(() { - twilioInit = true; - }); - } + if (success) { + setState(() { + twilioInit = true; + }); + } // } else { // // for web, we always show the initialisation screen unless we specified an // if (widget.registrationMethod == RegistrationMethod.env) { @@ -286,7 +287,7 @@ class _AppState extends State { switch (event) { case CallEvent.incoming: - // applies to web only + // applies to web only if (kIsWeb || Platform.isAndroid) { final activeCall = TwilioVoice.instance.call.activeCall; if (activeCall != null && activeCall.callDirection == CallDirection.incoming) { @@ -338,7 +339,8 @@ class _AppState extends State { showDialog( // ignore: use_build_context_synchronously context: context, - builder: (context) => const AlertDialog( + builder: (context) => + const AlertDialog( title: Text("Error"), content: Text("Failed to register for calls"), ), @@ -357,6 +359,10 @@ class _AppState extends State { appBar: AppBar( title: const Text("Plugin example app"), actions: [ + if(twilioInit) ...[ + const SizedBox(width: 8), + const _UpdateTokenAction(), + ], _LogoutAction( onSuccess: () { setState(() { @@ -381,12 +387,12 @@ class _AppState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: twilioInit ? UICallScreen( - userId: userId, - onPerformCall: _onPerformCall, - ) + userId: userId, + onPerformCall: _onPerformCall, + ) : UIRegistrationScreen( - onRegister: _onRegisterWithToken, - ), + onRegister: _onRegisterWithToken, + ), ), ), ), @@ -463,3 +469,30 @@ class _LogoutAction extends StatelessWidget { icon: const Icon(Icons.logout, color: Colors.white)); } } + +class _UpdateTokenAction extends StatelessWidget { + const _UpdateTokenAction({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: () async { + // show dialog to enter new token + final token = await showDialog( + context: context, + builder: (context) => const UpdateTokenDialogContent(), + ); + if (token?.isEmpty ?? true) { + return; + } + final result = await TwilioVoice.instance.setTokens(accessToken: token!); + final message = (result ?? false) ? "Successfully updated token" : "Failed to update token"; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + }, + label: const Text("Update Token", style: TextStyle(color: Colors.white)), + icon: const Icon(Icons.refresh, color: Colors.white), + ); + } +} diff --git a/lib/_internal/js/device/device.dart b/lib/_internal/js/device/device.dart index f10ecf68..f9e2bb02 100644 --- a/lib/_internal/js/device/device.dart +++ b/lib/_internal/js/device/device.dart @@ -87,6 +87,11 @@ class Device extends Twilio { /// Documentation: https://www.twilio.com/docs/voice/sdks/javascript/twiliodevice#devicestate @JS("status") external String state(); + + /// Update the device's access token. + /// Documentation: https://www.twilio.com/docs/voice/sdks/javascript/twiliodevice#deviceupdatetokentoken + @JS("updateToken") + external void updateToken(String token); } /// Device options diff --git a/lib/_internal/twilio_voice_web.dart b/lib/_internal/twilio_voice_web.dart index f8da2258..ee154e4f 100644 --- a/lib/_internal/twilio_voice_web.dart +++ b/lib/_internal/twilio_voice_web.dart @@ -363,6 +363,10 @@ class TwilioVoiceWeb extends MethodChannelTwilioVoice { // } // } try { + final shouldUpdate = device != null && getDeviceState(device!) == DeviceState.registered; + if (shouldUpdate) { + device!.updateToken(accessToken); + } else { /// opus set as primary code /// https://www.twilio.com/blog/client-javascript-sdk-1-7-ga List codecs = ["opus", "pcmu"]; @@ -378,6 +382,7 @@ class TwilioVoiceWeb extends MethodChannelTwilioVoice { // Register device to accept notifications device!.register(); + } return true; } catch (e) {