diff --git a/audio_call/android/app/build.gradle b/audio_call/android/app/build.gradle index 20f5ea6..d990581 100644 --- a/audio_call/android/app/build.gradle +++ b/audio_call/android/app/build.gradle @@ -37,7 +37,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.voximplant.flutter.audiocall" // set minSdk to 21 since multidex is enabled by default since this version - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/audio_call/ios/Podfile.lock b/audio_call/ios/Podfile.lock index 19e2f55..f060100 100644 --- a/audio_call/ios/Podfile.lock +++ b/audio_call/ios/Podfile.lock @@ -15,9 +15,9 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.8.0): + - FirebaseCoreInternal (10.25.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.8.0): + - FirebaseInstallations (10.25.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -36,46 +36,52 @@ PODS: - Flutter - flutter_local_notifications (0.0.1): - Flutter - - flutter_voximplant (3.8.0): + - flutter_voximplant (3.11.1): - Flutter - - VoxImplantSDK (= 2.46.12) - - GoogleDataTransport (9.2.2): + - VoxImplantSDK (= 2.52.0) + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.1): + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.1): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.1)" - - GoogleUtilities/Reachability (7.11.1): + - "GoogleUtilities/NSData+zlib (7.13.3)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.1): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) + - GoogleUtilities/Privacy + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - permission_handler_apple (9.0.4): - Flutter - - PromisesObjC (2.2.0) + - PromisesObjC (2.4.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - VoxImplantSDK (2.46.12): - - VoxImplantSDK/Core (= 2.46.12) - - VoxImplantSDK/Core (2.46.12): - - VoxImplantWebRTC (= 93.0.0) - - VoxImplantWebRTC (93.0.0) + - VoxImplantSDK (2.52.0): + - VoxImplantWebRTC (= 112.0.0) + - VoxImplantWebRTC (112.0.0) DEPENDENCIES: - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -124,22 +130,22 @@ SPEC CHECKSUMS: firebase_core: 58542d7399889ebdbb034baa72d081e54c5c814d firebase_messaging: 01a8db2962f81ea190d08db767aba2e7e805e647 FirebaseCore: fa80ad16a62d52f67274b5b88304c3a318bbf9a4 - FirebaseCoreInternal: fa2899eb1f340054858d289e5a0fb935a0a74e52 - FirebaseInstallations: b2a05a3fe707df764345d68685534d07d0664af3 + FirebaseCoreInternal: 910a81992c33715fec9263ca7381d59ab3a750b7 + FirebaseInstallations: 91950fe859846fff0fbd296180909dd273103b09 FirebaseMessaging: fd93783258c53ae5cdb9b41bf0c51606a677f2d5 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_callkit_voximplant: 9e1128135757ea8a21ae7bbc06ff4a37e906ccc5 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 - flutter_voximplant: 40c3110cc9943047f36fb9847f8803350237480a - GoogleDataTransport: 8378d1fa8ac49753ea6ce70d65a7cb70ce5f66e6 - GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + flutter_voximplant: f8a0e7ce1af495fe6ecd53233640df972952f43d + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce - PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 - VoxImplantSDK: 577259786bc3da6ed3cf3d4ee1a8e08245c19ce6 - VoxImplantWebRTC: 8ddd8a63d0c20afa604fec22a2552c32b0371974 + VoxImplantSDK: fbc3bfc64cedc98545758717c8395a64edc091bf + VoxImplantWebRTC: 2256a8b9157cc46e1a4c8e0ed5816c5c672d7d5d -PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 +PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 -COCOAPODS: 1.15.0 +COCOAPODS: 1.15.2 diff --git a/audio_call/lib/screens/active_call/bloc/active_call_bloc.dart b/audio_call/lib/screens/active_call/bloc/active_call_bloc.dart index 1ef19aa..f8bd105 100644 --- a/audio_call/lib/screens/active_call/bloc/active_call_bloc.dart +++ b/audio_call/lib/screens/active_call/bloc/active_call_bloc.dart @@ -50,19 +50,18 @@ class ActiveCallBloc extends Bloc { } Future _readyToStartCall( - ReadyToStartCallEvent event, - Emitter emit) async { + ReadyToStartCallEvent event, Emitter emit) async { _callStateSubscription = _callService.subscribeToCallEvents().listen( - (event) { + (event) { add(CallChangedEvent(event)); }, ); _audioDeviceSubscription = _callService.subscribeToAudioDeviceEvents().listen( - (event) { - add(AudioDevicesChanged(event)); - }, - ); + (event) { + add(AudioDevicesChanged(event)); + }, + ); try { if (_isIncoming) { if (Platform.isAndroid) { @@ -96,8 +95,7 @@ class ActiveCallBloc extends Bloc { } Future _handleCallChanged( - CallChangedEvent event, - Emitter emit) async { + CallChangedEvent event, Emitter emit) async { CallEvent callEvent = event.event; if (callEvent is OnFailedCallEvent) { @@ -145,38 +143,33 @@ class ActiveCallBloc extends Bloc { } Future _holdCall( - HoldPressedEvent event, - Emitter emit) async { - Platform.isIOS + HoldPressedEvent event, Emitter emit) async { + Platform.isIOS ? await _callKitService?.holdCall(event.hold) : await _callService.holdCall(hold: event.hold); } Future _muteAudio( - MutePressedEvent event, - Emitter emit) async { - Platform.isIOS + MutePressedEvent event, Emitter emit) async { + Platform.isIOS ? await _callKitService?.muteCall(event.mute) : await _callService.muteCall(mute: event.mute); } Future _hangupCall( - HangupPressedEvent event, - Emitter emit) async { - Platform.isIOS + HangupPressedEvent event, Emitter emit) async { + Platform.isIOS ? await _callKitService?.endCall() : await _callService.hangup(); } - Future _selectAudioDevice( - SelectAudioDevicePressedEvent event, + Future _selectAudioDevice(SelectAudioDevicePressedEvent event, Emitter emit) async { await _callService.selectAudioDevice(device: event.device); } Future _handleAudioDevicesChanged( - AudioDevicesChanged event, - Emitter emit) async { + AudioDevicesChanged event, Emitter emit) async { AudioDeviceEvent audioEvent = event.event; if (audioEvent is OnActiveAudioDeviceChanged) { emit(state.copyWith(activeAudioDevice: audioEvent.device)); diff --git a/audio_call/lib/screens/login/bloc/login_bloc.dart b/audio_call/lib/screens/login/bloc/login_bloc.dart index f58a9f5..815741b 100644 --- a/audio_call/lib/screens/login/bloc/login_bloc.dart +++ b/audio_call/lib/screens/login/bloc/login_bloc.dart @@ -16,8 +16,7 @@ class LoginBloc extends Bloc { } Future _loadLastUser( - LoadLastUser event, - Emitter emit) async { + LoadLastUser event, Emitter emit) async { // TODO(yulia) // if (Platform.isAndroid && // await NotificationHelper().didNotificationLaunchApp()) { @@ -35,23 +34,26 @@ class LoginBloc extends Bloc { await _authService.loginWithAccessToken(); emit(LoginSuccess()); } on VIException catch (e) { - emit(LoginFailure(errorCode: e.code, errorDescription: e.message ?? 'Unknown error')); + emit(LoginFailure( + errorCode: e.code, errorDescription: e.message ?? 'Unknown error')); } } } Future _loginWithPassword( - LoginWithPassword event, - Emitter emit) async { + LoginWithPassword event, Emitter emit) async { emit(LoginInProgress()); + VINode node = VINode.values.byName(event.node); try { await _authService.loginWithPassword( + node, '${event.username}.voximplant.com', event.password, ); emit(LoginSuccess()); } on VIException catch (e) { - emit(LoginFailure(errorCode: e.code, errorDescription: e.message ?? 'Unknown error')); + emit(LoginFailure( + errorCode: e.code, errorDescription: e.message ?? 'Unknown error')); } } } diff --git a/audio_call/lib/screens/login/bloc/login_event.dart b/audio_call/lib/screens/login/bloc/login_event.dart index 1b646ba..09f3d15 100644 --- a/audio_call/lib/screens/login/bloc/login_event.dart +++ b/audio_call/lib/screens/login/bloc/login_event.dart @@ -9,20 +9,22 @@ abstract class LoginEvent extends Equatable { List get props => []; } -class LoadLastUser extends LoginEvent { } +class LoadLastUser extends LoginEvent {} class LoginWithPassword extends LoginEvent { final String username; final String password; + final String node; - const LoginWithPassword({required this.username, required this.password}); + const LoginWithPassword( + {required this.username, required this.password, required this.node}); @override - List get props => [username, password]; + List get props => [username, password, node]; @override String toString() => 'LoginWithPassword: ' 'username: $username, password: *****'; } -class Dispose extends LoginEvent { } +class Dispose extends LoginEvent {} diff --git a/audio_call/lib/screens/login/login_page.dart b/audio_call/lib/screens/login/login_page.dart index fd4db6c..5a1442a 100644 --- a/audio_call/lib/screens/login/login_page.dart +++ b/audio_call/lib/screens/login/login_page.dart @@ -6,6 +6,19 @@ import 'package:audio_call/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +const List nodes = [ + 'Node1', + 'Node2', + 'Node3', + 'Node4', + 'Node5', + 'Node6', + 'Node7', + 'Node8', + 'Node9', + 'Node10' +]; + class LoginPage extends StatefulWidget { static const routeName = '/login'; @@ -22,6 +35,8 @@ class _LoginPageState extends State { bool _isUsernameValid = true; bool _isPasswordValid = true; + bool _isNodeSelected = false; + String? _node; @override void initState() { @@ -39,12 +54,18 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { - void login() => _bloc.add( + void login() { + final node = _node; + if (node != null) { + _bloc.add( LoginWithPassword( username: _usernameController.text, password: _passwordController.text, + node: node, ), ); + } + } void handleLoginFailed(String errorCode, String errorDescription) { if (errorCode == 'ERROR_INVALID_USERNAME') { @@ -92,6 +113,25 @@ class _LoginPageState extends State { validator: (_) => _isPasswordValid ? null : 'Invalid password', ), + _isNodeSelected + ? Container() + : const Padding( + padding: EdgeInsets.only(top: 5, left: 20), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + "Connection Node is required", + style: TextStyle(color: VoximplantColors.red), + ))), + Widgets.dropdown( + items: nodes, + onChange: (String? node) { + setState(() { + _isNodeSelected = true; + _node = node; + }); + }, + value: _node), Widgets.maxWidthRaisedButton( text: 'Log in', onPressed: login, diff --git a/audio_call/lib/screens/main/bloc/main_bloc.dart b/audio_call/lib/screens/main/bloc/main_bloc.dart index 33dc62f..1d81ec1 100644 --- a/audio_call/lib/screens/main/bloc/main_bloc.dart +++ b/audio_call/lib/screens/main/bloc/main_bloc.dart @@ -18,9 +18,13 @@ class MainBloc extends Bloc { final CallKitService? _callKitService = Platform.isIOS ? CallKitService() : null; + bool _logoutRequested = false; + late StreamSubscription _callStateSubscription; - MainBloc() : super(MainInitial(myDisplayName: AuthService().displayName ?? 'Unknown user')) { + MainBloc() + : super(MainInitial( + myDisplayName: AuthService().displayName ?? 'Unknown user')) { _authService.onDisconnected = () => add(ConnectionClosed()); _callStateSubscription = _callService.subscribeToCallEvents().listen(onCallEvent); @@ -38,41 +42,39 @@ class MainBloc extends Bloc { } Future _checkPermissions( - CheckPermissionsForCall event, - Emitter emit) async { - bool permissionsGranted = await checkPermissions(); + CheckPermissionsForCall event, Emitter emit) async { + bool permissionsGranted = await checkPermissions(); if (permissionsGranted) { - emit(PermissionCheckSuccess(myDisplayName: _authService.displayName ?? 'Unknown user')); + emit(PermissionCheckSuccess( + myDisplayName: _authService.displayName ?? 'Unknown user')); } else { - emit(PermissionCheckFail(myDisplayName: _authService.displayName ?? 'Unknown user')); + emit(PermissionCheckFail( + myDisplayName: _authService.displayName ?? 'Unknown user')); } } - Future _logout( - LogOut event, - Emitter emit) async { + Future _logout(LogOut event, Emitter emit) async { + _logoutRequested = true; await _authService.logout(); emit(const LoggedOut(networkIssues: false)); } Future _connectionClosed( - ConnectionClosed event, - Emitter emit) async { - emit(const LoggedOut(networkIssues: true)); + ConnectionClosed event, Emitter emit) async { + if (!_logoutRequested) { + emit(const LoggedOut(networkIssues: true)); + } } void _receivedIncomingCall( - ReceivedIncomingCall event, - Emitter emit) { + ReceivedIncomingCall event, Emitter emit) { emit(IncomingCall( caller: event.displayName, myDisplayName: _authService.displayName ?? 'Unknown user', )); } - Future _reconnect( - Reconnect event, - Emitter emit) async { + Future _reconnect(Reconnect event, Emitter emit) async { try { final displayName = await _authService.loginWithAccessToken(); if (displayName == null) { diff --git a/audio_call/lib/services/auth_service.dart b/audio_call/lib/services/auth_service.dart index f41dcd4..b25c1d3 100644 --- a/audio_call/lib/services/auth_service.dart +++ b/audio_call/lib/services/auth_service.dart @@ -42,7 +42,8 @@ class AuthService { }); } - Future loginWithPassword(String username, String password) async { + Future loginWithPassword( + VINode node, String username, String password) async { _log('loginWithPassword'); VIClientState clientState = await _client.getClientState(); if (clientState == VIClientState.LoggedIn) { @@ -51,8 +52,9 @@ class AuthService { return displayName; } } + if (clientState == VIClientState.Disconnected) { - await _client.connect(); + await _client.connect(node: node); } VIAuthResult authResult = await _client.login(username, password); if (_voipToken != null) { @@ -61,28 +63,34 @@ class AuthService { await _client.registerForPushNotifications(voipToken); } } - await _saveAuthDetails(username, authResult.loginTokens); + await _saveAuthDetails(username, authResult.loginTokens, node); _displayName = authResult.displayName; return _displayName ?? "Unknown user"; } Future loginWithAccessToken() async { VIClientState clientState = await _client.getClientState(); + SharedPreferences prefs = await SharedPreferences.getInstance(); if (clientState == VIClientState.LoggedIn) { return _displayName; } else if (clientState == VIClientState.Connecting || clientState == VIClientState.LoggingIn) { return null; } else if (clientState == VIClientState.Disconnected) { - await _client.connect(); + final node = _getNode(prefs); + if (node != null) { + await _client.connect(node: node); + } else { + throw Exception("Node is unknown"); + } } _log('loginWithAccessToken'); - SharedPreferences prefs = await SharedPreferences.getInstance(); final loginTokens = _getAuthDetails(prefs); String? user = prefs.getString('username'); if (user != null && loginTokens != null) { - VIAuthResult authResult = await _client.loginWithAccessToken(user, loginTokens.accessToken); - await _saveAuthDetails(user, authResult.loginTokens); + VIAuthResult authResult = + await _client.loginWithAccessToken(user, loginTokens.accessToken); + await _saveAuthDetails(user, authResult.loginTokens, null); _displayName = authResult.displayName; } final voipToken = _voipToken; @@ -96,6 +104,7 @@ class AuthService { _log('logout'); await _client.disconnect(); final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.remove("node"); prefs.remove('username'); prefs.remove('accessToken'); prefs.remove('refreshToken'); @@ -114,11 +123,14 @@ class AuthService { } Future _saveAuthDetails( - String username, VILoginTokens? loginTokens) async { + String username, VILoginTokens? loginTokens, VINode? node) async { if (loginTokens == null) { return; } final SharedPreferences prefs = await SharedPreferences.getInstance(); + if (node != null) { + prefs.setString('node', node.name); + } prefs.setString('username', username); prefs.setString('accessToken', loginTokens.accessToken); prefs.setString('refreshToken', loginTokens.refreshToken); @@ -126,18 +138,28 @@ class AuthService { prefs.setInt('refreshExpire', loginTokens.refreshExpire); } + VINode? _getNode(SharedPreferences prefs) { + final node = prefs.getString('node'); + if (node != null) { + return VINode.values.byName(node); + } + return null; + } + VILoginTokens? _getAuthDetails(SharedPreferences prefs) { final accessToken = prefs.getString('accessToken'); final refreshToken = prefs.getString('refreshToken'); final refreshExpire = prefs.getInt('refreshExpire'); final accessExpire = prefs.getInt('accessExpire'); - if (accessToken != null && refreshToken != null && refreshExpire != null && accessExpire != null) { + if (accessToken != null && + refreshToken != null && + refreshExpire != null && + accessExpire != null) { VILoginTokens loginTokens = VILoginTokens( - accessToken: accessToken, - refreshToken: refreshToken, - accessExpire: accessExpire, - refreshExpire: refreshExpire - ); + accessToken: accessToken, + refreshToken: refreshToken, + accessExpire: accessExpire, + refreshExpire: refreshExpire); return loginTokens; } return null; diff --git a/audio_call/lib/services/call/call_service.dart b/audio_call/lib/services/call/call_service.dart index 0474897..85291a1 100644 --- a/audio_call/lib/services/call/call_service.dart +++ b/audio_call/lib/services/call/call_service.dart @@ -25,8 +25,10 @@ class CallService { Function? onIncomingCall; - StreamController _callStreamController = StreamController.broadcast(); - StreamController _audioDeviceStreamController = StreamController.broadcast(); + StreamController _callStreamController = + StreamController.broadcast(); + StreamController _audioDeviceStreamController = + StreamController.broadcast(); CallState get callState => _callState; CallState _callState = CallState.none; @@ -174,11 +176,11 @@ class CallService { await _audioDeviceManager.selectAudioDevice(device); Future _onIncomingCall( - VIClient client, - VICall call, - bool video, - Map? headers, - ) async { + VIClient client, + VICall call, + bool video, + Map? headers, + ) async { _log('_onIncomingCall'); if (hasActiveCall && _activeCall?.callId != call.callId) { await call.reject(); @@ -215,10 +217,10 @@ class CallService { } void _onCallDisconnected( - VICall call, - Map? headers, - bool answeredElsewhere, - ) async { + VICall call, + Map? headers, + bool answeredElsewhere, + ) async { _log('onCallDisconnected'); if (call.callId == _activeCall?.callId) { _activeCall = null; @@ -239,11 +241,11 @@ class CallService { } void _onCallFailed( - VICall call, - int code, - String description, - Map? headers, - ) async { + VICall call, + int code, + String description, + Map? headers, + ) async { _log('onCallFailed($code, $description)'); if (call.callId == _activeCall?.callId) { _activeCall = null; diff --git a/audio_call/lib/services/call/callkit_service.dart b/audio_call/lib/services/call/callkit_service.dart index 06c7f38..6ed9fbd 100644 --- a/audio_call/lib/services/call/callkit_service.dart +++ b/audio_call/lib/services/call/callkit_service.dart @@ -108,12 +108,13 @@ class CallKitService { _provider.executeTransaction = (transaction) { _log('Should execute or delay transaction...'); - if ((_authService.clientState == VIClientState.LoggedIn || _authService.clientState == VIClientState.Reconnecting) - && (_callService.hasActiveCall || _callStarting) - ) { + if ((_authService.clientState == VIClientState.LoggedIn || + _authService.clientState == VIClientState.Reconnecting) && + (_callService.hasActiveCall || _callStarting)) { _log('Executing transaction now'); return false; - } else if (_authService.clientState == VIClientState.Disconnected || _authService.clientState == VIClientState.Connected) { + } else if (_authService.clientState == VIClientState.Disconnected || + _authService.clientState == VIClientState.Connected) { _log('Need to connect or login...'); Future loginAndCommitTransactions() async { try { @@ -128,6 +129,7 @@ class CallKitService { } } } + loginAndCommitTransactions(); } _log('Delaying transaction'); @@ -266,7 +268,7 @@ class CallKitService { Future _commitTransactions() async { List transactions = - await _provider.getPendingTransactions(); + await _provider.getPendingTransactions(); for (final transaction in transactions) { List actions = await transaction.getActions(); for (final action in actions) { @@ -393,11 +395,11 @@ class CallKitService { throw 'Active call is null, holdCall failed'; } final call = _activeCall?.call; - if (call == null || call.hasConnected) { + if (call == null || !call.hasConnected) { return; } - await _callController.requestTransactionWithAction( - FCXSetHeldCallAction(call.uuid, hold)); + await _callController + .requestTransactionWithAction(FCXSetHeldCallAction(call.uuid, hold)); } Future muteCall(bool mute) async { @@ -405,11 +407,11 @@ class CallKitService { throw 'Active call is null, muteCall failed'; } final call = _activeCall?.call; - if (call == null || call.hasConnected) { + if (call == null) { return; } - await _callController.requestTransactionWithAction( - FCXSetMutedCallAction(call.uuid, mute)); + await _callController + .requestTransactionWithAction(FCXSetMutedCallAction(call.uuid, mute)); } Future endCall() async { diff --git a/audio_call/lib/widgets/widgets.dart b/audio_call/lib/widgets/widgets.dart index f8475eb..00234fd 100644 --- a/audio_call/lib/widgets/widgets.dart +++ b/audio_call/lib/widgets/widgets.dart @@ -72,7 +72,8 @@ class Widgets { width: double.infinity, height: 50, child: ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(color: VoximplantColors.white)), + style: ElevatedButton.styleFrom( + textStyle: const TextStyle(color: VoximplantColors.white)), onPressed: onPressed, child: Text(text), ), @@ -124,4 +125,50 @@ class Widgets { ), ); } + + static Widget dropdown( + {required List items, + required ValueChanged onChange, + String? value}) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 20, + ), + child: Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1.0, + style: BorderStyle.solid, + color: VoximplantColors.white), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + // color: VoximplantColors.white, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: DropdownButton( + value: value, + style: const TextStyle(color: VoximplantColors.white), + hint: const Text( + 'Connection node', + style: TextStyle(color: VoximplantColors.white), + ), + isExpanded: true, + dropdownColor: VoximplantColors.primary, + borderRadius: const BorderRadius.all(Radius.circular(4)), + alignment: AlignmentDirectional.center, + onChanged: (String? node) { + onChange(node); + }, + items: items.map>((String item) { + return DropdownMenuItem(value: item, child: Text(item)); + }).toList(), + ), + )), + ); + } } diff --git a/audio_call/pubspec.lock b/audio_call/pubspec.lock index 74d74fd..97d9f6d 100644 --- a/audio_call/pubspec.lock +++ b/audio_call/pubspec.lock @@ -231,10 +231,10 @@ packages: dependency: "direct dev" description: name: flutter_voximplant - sha256: "712fb316c1d6553326d29c43d17454cf69adc930527308af9136aa6c72921d4e" + sha256: f83ba01d70ce779427ae50d99744f5376d55ffadcb1f4cca2fcba342b280d169 url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.12.0" flutter_web_plugins: dependency: transitive description: flutter @@ -252,26 +252,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -300,10 +300,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" nested: dependency: transitive description: @@ -529,10 +529,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timezone: dependency: transitive description: @@ -569,10 +569,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" win32: dependency: transitive description: @@ -599,4 +599,4 @@ packages: version: "6.2.2" sdks: dart: ">=3.3.1 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/audio_call/pubspec.yaml b/audio_call/pubspec.yaml index da85360..1a7a918 100644 --- a/audio_call/pubspec.yaml +++ b/audio_call/pubspec.yaml @@ -17,7 +17,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^2.0.0 flutter_bloc: 8.1.2 - flutter_voximplant: 3.8.0 + flutter_voximplant: 3.12.0 flutter_callkit_voximplant: 2.1.0 uuid: 3.0.7 flutter_local_notifications: 13.0.0 diff --git a/video_call/android/app/build.gradle b/video_call/android/app/build.gradle index ac230a0..0255996 100644 --- a/video_call/android/app/build.gradle +++ b/video_call/android/app/build.gradle @@ -37,7 +37,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.voximplant.flutter.videocall" // set minSdk to 21 since multidex is enabled by default since this version - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/video_call/ios/Podfile.lock b/video_call/ios/Podfile.lock index 237088a..52a817d 100644 --- a/video_call/ios/Podfile.lock +++ b/video_call/ios/Podfile.lock @@ -15,9 +15,9 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.13.0): + - FirebaseCoreInternal (10.25.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.13.0): + - FirebaseInstallations (10.25.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -36,46 +36,52 @@ PODS: - Flutter - flutter_local_notifications (0.0.1): - Flutter - - flutter_voximplant (3.8.1): + - flutter_voximplant (3.11.1): - Flutter - - VoxImplantSDK (= 2.46.12) - - GoogleDataTransport (9.2.5): + - VoxImplantSDK (= 2.52.0) + - GoogleDataTransport (9.4.1): - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.5): + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.5)" - - GoogleUtilities/Reachability (7.11.5): + - "GoogleUtilities/NSData+zlib (7.13.3)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) + - GoogleUtilities/Privacy + - nanopb (2.30909.1): + - nanopb/decode (= 2.30909.1) + - nanopb/encode (= 2.30909.1) + - nanopb/decode (2.30909.1) + - nanopb/encode (2.30909.1) - permission_handler_apple (9.1.1): - Flutter - - PromisesObjC (2.3.1) + - PromisesObjC (2.4.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - VoxImplantSDK (2.46.12): - - VoxImplantSDK/Core (= 2.46.12) - - VoxImplantSDK/Core (2.46.12): - - VoxImplantWebRTC (= 93.0.0) - - VoxImplantWebRTC (93.0.0) + - VoxImplantSDK (2.52.0): + - VoxImplantWebRTC (= 112.0.0) + - VoxImplantWebRTC (112.0.0) DEPENDENCIES: - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -124,22 +130,22 @@ SPEC CHECKSUMS: firebase_core: 58542d7399889ebdbb034baa72d081e54c5c814d firebase_messaging: 01a8db2962f81ea190d08db767aba2e7e805e647 FirebaseCore: fa80ad16a62d52f67274b5b88304c3a318bbf9a4 - FirebaseCoreInternal: b342e37cd4f5b4454ec34308f073420e7920858e - FirebaseInstallations: b28af1b9f997f1a799efe818c94695a3728c352f + FirebaseCoreInternal: 910a81992c33715fec9263ca7381d59ab3a750b7 + FirebaseInstallations: 91950fe859846fff0fbd296180909dd273103b09 FirebaseMessaging: fd93783258c53ae5cdb9b41bf0c51606a677f2d5 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_callkit_voximplant: 9e1128135757ea8a21ae7bbc06ff4a37e906ccc5 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 - flutter_voximplant: 3a9f36552a8fdd6a6708e4027629bf49a807b7ee - GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2 - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + flutter_voximplant: f8a0e7ce1af495fe6ecd53233640df972952f43d + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c - VoxImplantSDK: 577259786bc3da6ed3cf3d4ee1a8e08245c19ce6 - VoxImplantWebRTC: 8ddd8a63d0c20afa604fec22a2552c32b0371974 + VoxImplantSDK: fbc3bfc64cedc98545758717c8395a64edc091bf + VoxImplantWebRTC: 2256a8b9157cc46e1a4c8e0ed5816c5c672d7d5d PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 -COCOAPODS: 1.15.0 +COCOAPODS: 1.15.2 diff --git a/video_call/ios/Runner.xcodeproj/project.pbxproj b/video_call/ios/Runner.xcodeproj/project.pbxproj index 628cf57..6e20cc7 100644 --- a/video_call/ios/Runner.xcodeproj/project.pbxproj +++ b/video_call/ios/Runner.xcodeproj/project.pbxproj @@ -203,7 +203,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -218,7 +218,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/video_call/lib/screens/login/bloc/login_bloc.dart b/video_call/lib/screens/login/bloc/login_bloc.dart index 006603d..4496528 100644 --- a/video_call/lib/screens/login/bloc/login_bloc.dart +++ b/video_call/lib/screens/login/bloc/login_bloc.dart @@ -1,5 +1,4 @@ /// Copyright (c) 2011-2020, Zingaya, Inc. All rights reserved. -import 'dart:io'; import 'package:video_call/screens/login/login.dart'; import 'package:video_call/services/auth_service.dart'; @@ -42,8 +41,10 @@ class LoginBloc extends Bloc { Future _loginWithPassword( LoginWithPassword event, Emitter emit) async { emit(LoginInProgress()); + VINode node = VINode.values.byName(event.node); try { await _authService.loginWithPassword( + node, '${event.username}.voximplant.com', event.password, ); diff --git a/video_call/lib/screens/login/bloc/login_event.dart b/video_call/lib/screens/login/bloc/login_event.dart index b99a2e5..09f3d15 100644 --- a/video_call/lib/screens/login/bloc/login_event.dart +++ b/video_call/lib/screens/login/bloc/login_event.dart @@ -14,11 +14,13 @@ class LoadLastUser extends LoginEvent {} class LoginWithPassword extends LoginEvent { final String username; final String password; + final String node; - const LoginWithPassword({required this.username, required this.password}); + const LoginWithPassword( + {required this.username, required this.password, required this.node}); @override - List get props => [username, password]; + List get props => [username, password, node]; @override String toString() => 'LoginWithPassword: ' diff --git a/video_call/lib/screens/login/login_page.dart b/video_call/lib/screens/login/login_page.dart index fe57c8c..4b7b6b9 100644 --- a/video_call/lib/screens/login/login_page.dart +++ b/video_call/lib/screens/login/login_page.dart @@ -6,6 +6,19 @@ import 'package:video_call/theme/voximplant_theme.dart'; import 'package:video_call/utils/navigation_helper.dart'; import 'package:video_call/widgets/widgets.dart'; +const List nodes = [ + 'Node1', + 'Node2', + 'Node3', + 'Node4', + 'Node5', + 'Node6', + 'Node7', + 'Node8', + 'Node9', + 'Node10' +]; + class LoginPage extends StatefulWidget { static const routeName = '/login'; @@ -22,6 +35,8 @@ class _LoginPageState extends State { bool _isUsernameValid = true; bool _isPasswordValid = true; + bool _isNodeSelected = false; + String? _node; @override void initState() { @@ -38,9 +53,18 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { - void _login() => _bloc.add(LoginWithPassword( - username: _usernameController.text, - password: _passwordController.text)); + void _login() { + final node = _node; + if (node != null) { + _bloc.add( + LoginWithPassword( + username: _usernameController.text, + password: _passwordController.text, + node: node, + ), + ); + } + } void _handleLoginFailed(String errorCode, String errorDescription) { if (errorCode == 'ERROR_INVALID_USERNAME') { @@ -89,6 +113,25 @@ class _LoginPageState extends State { validator: (_) => _isPasswordValid ? null : 'Invalid password', ), + _isNodeSelected + ? Container() + : const Padding( + padding: EdgeInsets.only(top: 5, left: 20), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + "Connection Node is required", + style: TextStyle(color: VoximplantColors.red), + ))), + Widgets.dropdown( + items: nodes, + onChange: (String? node) { + setState(() { + _isNodeSelected = true; + _node = node; + }); + }, + value: _node), Widgets.maxWidthRaisedButton( text: 'Log in', onPressed: _login, diff --git a/video_call/lib/screens/main/bloc/main_bloc.dart b/video_call/lib/screens/main/bloc/main_bloc.dart index 95b872d..0aa2860 100644 --- a/video_call/lib/screens/main/bloc/main_bloc.dart +++ b/video_call/lib/screens/main/bloc/main_bloc.dart @@ -18,6 +18,7 @@ class MainBloc extends Bloc { final CallKitService? _callKitService = Platform.isIOS ? CallKitService() : null; + bool _logoutRequested = false; late StreamSubscription _callStateSubscription; MainBloc() @@ -52,13 +53,16 @@ class MainBloc extends Bloc { } Future _logout(LogOut event, Emitter emit) async { + _logoutRequested = true; await _authService.logout(); emit(const LoggedOut(networkIssues: false)); } Future _connectionClosed( ConnectionClosed event, Emitter emit) async { - emit(const LoggedOut(networkIssues: true)); + if (!_logoutRequested) { + emit(const LoggedOut(networkIssues: true)); + } } void _receivedIncomingCall( diff --git a/video_call/lib/services/auth_service.dart b/video_call/lib/services/auth_service.dart index f37c7d8..a51540f 100644 --- a/video_call/lib/services/auth_service.dart +++ b/video_call/lib/services/auth_service.dart @@ -42,7 +42,8 @@ class AuthService { }); } - Future loginWithPassword(String username, String password) async { + Future loginWithPassword( + VINode node, String username, String password) async { _log('loginWithPassword'); VIClientState clientState = await _client.getClientState(); if (clientState == VIClientState.LoggedIn) { @@ -52,7 +53,7 @@ class AuthService { } } if (clientState == VIClientState.Disconnected) { - await _client.connect(); + await _client.connect(node: node); } VIAuthResult authResult = await _client.login(username, password); if (_voipToken != null) { @@ -61,28 +62,34 @@ class AuthService { await _client.registerForPushNotifications(voipToken); } } - await _saveAuthDetails(username, authResult.loginTokens); + await _saveAuthDetails(username, authResult.loginTokens, node); _displayName = authResult.displayName; return _displayName ?? "Unknown user"; } Future loginWithAccessToken() async { VIClientState clientState = await _client.getClientState(); + SharedPreferences prefs = await SharedPreferences.getInstance(); if (clientState == VIClientState.LoggedIn) { return _displayName; } else if (clientState == VIClientState.Connecting || clientState == VIClientState.LoggingIn) { return null; } else if (clientState == VIClientState.Disconnected) { - await _client.connect(); + final node = _getNode(prefs); + if (node != null) { + await _client.connect(node: node); + } else { + throw Exception("Node is unknown"); + } } _log('loginWithAccessToken'); - SharedPreferences prefs = await SharedPreferences.getInstance(); final loginTokens = _getAuthDetails(prefs); String? user = prefs.getString('username'); if (user != null && loginTokens != null) { - VIAuthResult authResult = await _client.loginWithAccessToken(user, loginTokens.accessToken); - await _saveAuthDetails(user, authResult.loginTokens); + VIAuthResult authResult = + await _client.loginWithAccessToken(user, loginTokens.accessToken); + await _saveAuthDetails(user, authResult.loginTokens, null); _displayName = authResult.displayName; } final voipToken = _voipToken; @@ -96,6 +103,7 @@ class AuthService { _log('logout'); await _client.disconnect(); final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.remove("node"); prefs.remove('username'); prefs.remove('accessToken'); prefs.remove('refreshToken'); @@ -114,11 +122,14 @@ class AuthService { } Future _saveAuthDetails( - String username, VILoginTokens? loginTokens) async { + String username, VILoginTokens? loginTokens, VINode? node) async { if (loginTokens == null) { return; } final SharedPreferences prefs = await SharedPreferences.getInstance(); + if (node != null) { + prefs.setString('node', node.name); + } prefs.setString('username', username); prefs.setString('accessToken', loginTokens.accessToken); prefs.setString('refreshToken', loginTokens.refreshToken); @@ -126,18 +137,28 @@ class AuthService { prefs.setInt('refreshExpire', loginTokens.refreshExpire); } + VINode? _getNode(SharedPreferences prefs) { + final node = prefs.getString('node'); + if (node != null) { + return VINode.values.byName(node); + } + return null; + } + VILoginTokens? _getAuthDetails(SharedPreferences prefs) { final accessToken = prefs.getString('accessToken'); final refreshToken = prefs.getString('refreshToken'); final refreshExpire = prefs.getInt('refreshExpire'); final accessExpire = prefs.getInt('accessExpire'); - if (accessToken != null && refreshToken != null && refreshExpire != null && accessExpire != null) { + if (accessToken != null && + refreshToken != null && + refreshExpire != null && + accessExpire != null) { VILoginTokens loginTokens = VILoginTokens( accessToken: accessToken, refreshToken: refreshToken, accessExpire: accessExpire, - refreshExpire: refreshExpire - ); + refreshExpire: refreshExpire); return loginTokens; } return null; @@ -152,4 +173,3 @@ class AuthService { log('AuthService($hashCode): ${message.toString()}'); } } - diff --git a/video_call/lib/widgets/widgets.dart b/video_call/lib/widgets/widgets.dart index af910a8..dbfe1f8 100644 --- a/video_call/lib/widgets/widgets.dart +++ b/video_call/lib/widgets/widgets.dart @@ -21,11 +21,11 @@ class Widgets { child: Theme( data: ThemeData( primaryColor: - darkBackground ? VoximplantColors.white : VoximplantColors.button, + darkBackground ? VoximplantColors.white : VoximplantColors.button, // cursorColor: // darkBackground ? VoximplantColors.white : VoximplantColors.button, hintColor: - darkBackground ? VoximplantColors.white : VoximplantColors.button, + darkBackground ? VoximplantColors.white : VoximplantColors.button, ), child: TextFormField( decoration: InputDecoration( @@ -72,7 +72,8 @@ class Widgets { width: double.infinity, height: 50, child: ElevatedButton( - style: ElevatedButton.styleFrom(textStyle: const TextStyle(color: VoximplantColors.white)), + style: ElevatedButton.styleFrom( + textStyle: const TextStyle(color: VoximplantColors.white)), onPressed: onPressed, child: Text(text), ), @@ -124,4 +125,50 @@ class Widgets { ), ); } + + static Widget dropdown( + {required List items, + required ValueChanged onChange, + String? value}) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 20, + ), + child: Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1.0, + style: BorderStyle.solid, + color: VoximplantColors.white), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + ), + // color: VoximplantColors.white, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: DropdownButton( + value: value, + style: const TextStyle(color: VoximplantColors.white), + hint: const Text( + 'Connection node', + style: TextStyle(color: VoximplantColors.white), + ), + isExpanded: true, + dropdownColor: VoximplantColors.primary, + borderRadius: const BorderRadius.all(Radius.circular(4)), + alignment: AlignmentDirectional.center, + onChanged: (String? node) { + onChange(node); + }, + items: items.map>((String item) { + return DropdownMenuItem(value: item, child: Text(item)); + }).toList(), + ), + )), + ); + } } diff --git a/video_call/pubspec.lock b/video_call/pubspec.lock index abf44e6..e76e486 100644 --- a/video_call/pubspec.lock +++ b/video_call/pubspec.lock @@ -223,10 +223,10 @@ packages: dependency: "direct main" description: name: flutter_voximplant - sha256: "6d57d456b176189c79dd2e6adcf294bdb3cd2bdeee101ba94cc8ac2e9cabe86f" + sha256: f83ba01d70ce779427ae50d99744f5376d55ffadcb1f4cca2fcba342b280d169 url: "https://pub.dev" source: hosted - version: "3.8.1" + version: "3.12.0" flutter_web_plugins: dependency: transitive description: flutter diff --git a/video_call/pubspec.yaml b/video_call/pubspec.yaml index f0731d2..2519b38 100644 --- a/video_call/pubspec.yaml +++ b/video_call/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_lints: ^2.0.0 flutter_bloc: 8.1.2 - flutter_voximplant: 3.8.1 + flutter_voximplant: 3.12.0 flutter_callkit_voximplant: 2.1.0 uuid: 3.0.7 flutter_local_notifications: 13.0.0