From dbc1e5ea3e7bd11745e3256c6faede493b1af74d Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 5 Jul 2022 17:37:06 -0300 Subject: [PATCH 01/23] Fix crash when trying to Focus an Entry from a background thread and improved the code so there are fewer direct access from the VM to the View (#1879) --- src/App/Pages/Accounts/LockPage.xaml.cs | 39 ++++++++++++++------- src/App/Pages/Accounts/LockPageViewModel.cs | 30 +++++++--------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/App/Pages/Accounts/LockPage.xaml.cs b/src/App/Pages/Accounts/LockPage.xaml.cs index 354c45230..22f6416ef 100644 --- a/src/App/Pages/Accounts/LockPage.xaml.cs +++ b/src/App/Pages/Accounts/LockPage.xaml.cs @@ -25,8 +25,6 @@ namespace Bit.App.Pages _vm = BindingContext as LockPageViewModel; _vm.Page = this; _vm.UnlockedAction = () => Device.BeginInvokeOnMainThread(async () => await UnlockedAsync()); - MasterPasswordEntry = _masterPassword; - PinEntry = _pin; if (Device.RuntimePlatform == Device.iOS) { @@ -38,8 +36,17 @@ namespace Bit.App.Pages } } - public Entry MasterPasswordEntry { get; set; } - public Entry PinEntry { get; set; } + public Entry SecretEntry + { + get + { + if (_vm?.PinLock ?? false) + { + return _pin; + } + return _masterPassword; + } + } public async Task PromptBiometricAfterResumeAsync() { @@ -70,16 +77,12 @@ namespace Bit.App.Pages _vm.AvatarImageSource = await GetAvatarImageSourceAsync(); await _vm.InitAsync(); + + _vm.FocusSecretEntry += PerformFocusSecretEntry; + if (!_vm.BiometricLock) { - if (_vm.PinLock) - { - RequestFocus(PinEntry); - } - else - { - RequestFocus(MasterPasswordEntry); - } + RequestFocus(SecretEntry); } else { @@ -99,6 +102,18 @@ namespace Bit.App.Pages } } + private void PerformFocusSecretEntry(int? cursorPosition) + { + Device.BeginInvokeOnMainThread(() => + { + SecretEntry.Focus(); + if (cursorPosition.HasValue) + { + SecretEntry.CursorPosition = cursorPosition.Value; + } + }); + } + protected override bool OnBackButtonPressed() { if (_accountListOverlay.IsVisible) diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index 5b007be38..0b771ae1e 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -10,6 +10,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.Helpers; using Xamarin.Forms; namespace Bit.App.Pages @@ -27,6 +28,7 @@ namespace Bit.App.Pages private readonly IBiometricService _biometricService; private readonly IKeyConnectorService _keyConnectorService; private readonly ILogger _logger; + private readonly WeakEventManager _secretEntryFocusWeakEventManager = new WeakEventManager(); private string _email; private bool _showPassword; @@ -133,6 +135,11 @@ namespace Bit.App.Pages public string MasterPassword { get; set; } public string Pin { get; set; } public Action UnlockedAction { get; set; } + public event Action FocusSecretEntry + { + add => _secretEntryFocusWeakEventManager.AddEventHandler(value); + remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value); + } public async Task InitAsync() { @@ -342,15 +349,12 @@ namespace Bit.App.Pages _messagingService.Send("logout"); } } - + public void TogglePassword() { ShowPassword = !ShowPassword; - var page = (Page as LockPage); - var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry; - var str = PinLock ? Pin : MasterPassword; - entry.Focus(); - entry.CursorPosition = String.IsNullOrEmpty(str) ? 0 : str.Length; + var secret = PinLock ? Pin : MasterPassword; + _secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry)); } public async Task PromptBiometricAsync() @@ -361,18 +365,8 @@ namespace Bit.App.Pages return; } var success = await _platformUtilsService.AuthenticateBiometricAsync(null, - PinLock ? AppResources.PIN : AppResources.MasterPassword, () => - { - var page = Page as LockPage; - if (PinLock) - { - page.PinEntry.Focus(); - } - else - { - page.MasterPasswordEntry.Focus(); - } - }); + PinLock ? AppResources.PIN : AppResources.MasterPassword, + () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry))); await _stateService.SetBiometricLockedAsync(!success); if (success) { From 448cce38e1968954b809c9f06c5b877e6bfd1aeb Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 5 Jul 2022 18:14:10 -0300 Subject: [PATCH 02/23] Improved BroadcastService and added try...catch on async void callbacks (#1917) --- src/Android/MainApplication.cs | 9 +- src/App/App.xaml.cs | 154 +++++++------- src/App/Pages/Accounts/HomePage.xaml.cs | 2 +- .../SendGroupingsPage.xaml.cs | 32 +-- .../Pages/Vault/AutofillCiphersPage.xaml.cs | 32 +-- .../Vault/GroupingsPage/GroupingsPage.xaml.cs | 32 +-- src/App/Pages/Vault/ViewPage.xaml.cs | 56 ++--- src/Core/Abstractions/IBroadcasterService.cs | 3 +- src/Core/Services/BroadcasterService.cs | 63 ++++-- .../Renderers/CustomTabbedRenderer.cs | 2 +- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 8 +- src/iOS/AppDelegate.cs | 199 +++++++++--------- 12 files changed, 335 insertions(+), 257 deletions(-) diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index ecb9ca277..991aea474 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -99,12 +99,13 @@ namespace Bit.Droid { ServiceContainer.Register("nativeLogService", new AndroidLogService()); #if FDROID - ServiceContainer.Register("logger", new StubLogger()); + var logger = new StubLogger(); #elif DEBUG - ServiceContainer.Register("logger", DebugLogger.Instance); + var logger = DebugLogger.Instance; #else - ServiceContainer.Register("logger", Logger.Instance); + var logger = Logger.Instance; #endif + ServiceContainer.Register("logger", logger); // Note: This might cause a race condition. Investigate more. Task.Run(() => @@ -124,7 +125,7 @@ namespace Bit.Droid var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db")); var localizeService = new LocalizeService(); - var broadcasterService = new BroadcasterService(); + var broadcasterService = new BroadcasterService(logger); var messagingService = new MobileBroadcasterMessagingService(broadcasterService); var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo()); var secureStorageService = new SecureStorageService(); diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 9e534cd72..3415fd7e9 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -10,6 +10,7 @@ using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; using Xamarin.Forms.Xaml; @@ -56,86 +57,93 @@ namespace Bit.App Bootstrap(); _broadcasterService.Subscribe(nameof(App), async (message) => { - if (message.Command == "showDialog") + try { - var details = message.Data as DialogDetails; - var confirmed = true; - var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? - AppResources.Ok : details.ConfirmText; - Device.BeginInvokeOnMainThread(async () => + if (message.Command == "showDialog") { - if (!string.IsNullOrWhiteSpace(details.CancelText)) + var details = message.Data as DialogDetails; + var confirmed = true; + var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ? + AppResources.Ok : details.ConfirmText; + Device.BeginInvokeOnMainThread(async () => { - confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText, - details.CancelText); - } - else - { - await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText); - } - _messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); - }); - } - else if (message.Command == "resumed") - { - if (Device.RuntimePlatform == Device.iOS) + if (!string.IsNullOrWhiteSpace(details.CancelText)) + { + confirmed = await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText, + details.CancelText); + } + else + { + await Current.MainPage.DisplayAlert(details.Title, details.Text, confirmText); + } + _messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed)); + }); + } + else if (message.Command == "resumed") { - ResumedAsync().FireAndForget(); + if (Device.RuntimePlatform == Device.iOS) + { + ResumedAsync().FireAndForget(); + } + } + else if (message.Command == "slept") + { + if (Device.RuntimePlatform == Device.iOS) + { + await SleptAsync(); + } + } + else if (message.Command == "migrated") + { + await Task.Delay(1000); + await _accountsManager.NavigateOnAccountChangeAsync(); + } + else if (message.Command == "popAllAndGoToTabGenerator" || + message.Command == "popAllAndGoToTabMyVault" || + message.Command == "popAllAndGoToTabSend" || + message.Command == "popAllAndGoToAutofillCiphers") + { + Device.BeginInvokeOnMainThread(async () => + { + if (Current.MainPage is TabsPage tabsPage) + { + while (tabsPage.Navigation.ModalStack.Count > 0) + { + await tabsPage.Navigation.PopModalAsync(false); + } + if (message.Command == "popAllAndGoToAutofillCiphers") + { + Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); + } + else if (message.Command == "popAllAndGoToTabMyVault") + { + Options.MyVaultTile = false; + tabsPage.ResetToVaultPage(); + } + else if (message.Command == "popAllAndGoToTabGenerator") + { + Options.GeneratorTile = false; + tabsPage.ResetToGeneratorPage(); + } + else if (message.Command == "popAllAndGoToTabSend") + { + tabsPage.ResetToSendPage(); + } + } + }); + } + else if (message.Command == "convertAccountToKeyConnector") + { + Device.BeginInvokeOnMainThread(async () => + { + await Application.Current.MainPage.Navigation.PushModalAsync( + new NavigationPage(new RemoveMasterPasswordPage())); + }); } } - else if (message.Command == "slept") + catch (Exception ex) { - if (Device.RuntimePlatform == Device.iOS) - { - await SleptAsync(); - } - } - else if (message.Command == "migrated") - { - await Task.Delay(1000); - await _accountsManager.NavigateOnAccountChangeAsync(); - } - else if (message.Command == "popAllAndGoToTabGenerator" || - message.Command == "popAllAndGoToTabMyVault" || - message.Command == "popAllAndGoToTabSend" || - message.Command == "popAllAndGoToAutofillCiphers") - { - Device.BeginInvokeOnMainThread(async () => - { - if (Current.MainPage is TabsPage tabsPage) - { - while (tabsPage.Navigation.ModalStack.Count > 0) - { - await tabsPage.Navigation.PopModalAsync(false); - } - if (message.Command == "popAllAndGoToAutofillCiphers") - { - Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options)); - } - else if (message.Command == "popAllAndGoToTabMyVault") - { - Options.MyVaultTile = false; - tabsPage.ResetToVaultPage(); - } - else if (message.Command == "popAllAndGoToTabGenerator") - { - Options.GeneratorTile = false; - tabsPage.ResetToGeneratorPage(); - } - else if (message.Command == "popAllAndGoToTabSend") - { - tabsPage.ResetToSendPage(); - } - } - }); - } - else if (message.Command == "convertAccountToKeyConnector") - { - Device.BeginInvokeOnMainThread(async () => - { - await Application.Current.MainPage.Navigation.PushModalAsync( - new NavigationPage(new RemoveMasterPasswordPage())); - }); + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); } diff --git a/src/App/Pages/Accounts/HomePage.xaml.cs b/src/App/Pages/Accounts/HomePage.xaml.cs index d63413216..f01fccef0 100644 --- a/src/App/Pages/Accounts/HomePage.xaml.cs +++ b/src/App/Pages/Accounts/HomePage.xaml.cs @@ -54,7 +54,7 @@ namespace Bit.App.Pages { _vm.AvatarImageSource = await GetAvatarImageSourceAsync(); } - _broadcasterService.Subscribe(nameof(HomePage), async (message) => + _broadcasterService.Subscribe(nameof(HomePage), (message) => { if (message.Command == "updatedTheme") { diff --git a/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs b/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs index 183a832d8..6e8acc1ca 100644 --- a/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs +++ b/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPage.xaml.cs @@ -5,6 +5,7 @@ using Bit.App.Controls; using Bit.App.Models; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -68,21 +69,28 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(_pageName, async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted" || message.Command == "sendUpdated") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (_vm.LoadedOnce) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted" || message.Command == "sendUpdated") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var task = _vm.LoadAsync(); - } - }); + IsBusy = false; + if (_vm.LoadedOnce) + { + var task = _vm.LoadAsync(); + } + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); diff --git a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs index aed80316f..35b85fc9c 100644 --- a/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs +++ b/src/App/Pages/Vault/AutofillCiphersPage.xaml.cs @@ -6,6 +6,7 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -55,21 +56,28 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (_vm.LoadedOnce) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var task = _vm.LoadAsync(); - } - }); + IsBusy = false; + if (_vm.LoadedOnce) + { + var task = _vm.LoadAsync(); + } + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs index aa9fb56a7..29ae16989 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml.cs @@ -7,6 +7,7 @@ using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -95,21 +96,28 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(_pageName, async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (_vm.LoadedOnce) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var task = _vm.LoadAsync(); - } - }); + IsBusy = false; + if (_vm.LoadedOnce) + { + var task = _vm.LoadAsync(); + } + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); diff --git a/src/App/Pages/Vault/ViewPage.xaml.cs b/src/App/Pages/Vault/ViewPage.xaml.cs index eb03551f1..93e8b5eff 100644 --- a/src/App/Pages/Vault/ViewPage.xaml.cs +++ b/src/App/Pages/Vault/ViewPage.xaml.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Bit.App.Resources; using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; using Xamarin.Forms; @@ -56,37 +57,44 @@ namespace Bit.App.Pages _broadcasterService.Subscribe(nameof(ViewPage), async (message) => { - if (message.Command == "syncStarted") + try { - Device.BeginInvokeOnMainThread(() => IsBusy = true); - } - else if (message.Command == "syncCompleted") - { - await Task.Delay(500); - Device.BeginInvokeOnMainThread(() => + if (message.Command == "syncStarted") { - IsBusy = false; - if (message.Data is Dictionary data && data.ContainsKey("successfully")) + Device.BeginInvokeOnMainThread(() => IsBusy = true); + } + else if (message.Command == "syncCompleted") + { + await Task.Delay(500); + Device.BeginInvokeOnMainThread(() => { - var success = data["successfully"] as bool?; - if (success.GetValueOrDefault()) + IsBusy = false; + if (message.Data is Dictionary data && data.ContainsKey("successfully")) { - var task = _vm.LoadAsync(() => AdjustToolbar()); + var success = data["successfully"] as bool?; + if (success.GetValueOrDefault()) + { + var task = _vm.LoadAsync(() => AdjustToolbar()); + } } - } - }); - } - else if (message.Command == "selectSaveFileResult") - { - Device.BeginInvokeOnMainThread(() => + }); + } + else if (message.Command == "selectSaveFileResult") { - var data = message.Data as Tuple; - if (data == null) + Device.BeginInvokeOnMainThread(() => { - return; - } - _vm.SaveFileSelected(data.Item1, data.Item2); - }); + var data = message.Data as Tuple; + if (data == null) + { + return; + } + _vm.SaveFileSelected(data.Item1, data.Item2); + }); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); await LoadOnAppearedAsync(_scrollView, true, async () => diff --git a/src/Core/Abstractions/IBroadcasterService.cs b/src/Core/Abstractions/IBroadcasterService.cs index ac481b53d..0675aad18 100644 --- a/src/Core/Abstractions/IBroadcasterService.cs +++ b/src/Core/Abstractions/IBroadcasterService.cs @@ -5,7 +5,8 @@ namespace Bit.Core.Abstractions { public interface IBroadcasterService { - void Send(Message message, string id = null); + void Send(Message message); + void Send(Message message, string id); void Subscribe(string id, Action messageCallback); void Unsubscribe(string id); } diff --git a/src/Core/Services/BroadcasterService.cs b/src/Core/Services/BroadcasterService.cs index 553ec49c9..782529335 100644 --- a/src/Core/Services/BroadcasterService.cs +++ b/src/Core/Services/BroadcasterService.cs @@ -8,24 +8,58 @@ namespace Bit.App.Services { public class BroadcasterService : IBroadcasterService { + private readonly ILogger _logger; private readonly Dictionary> _subscribers = new Dictionary>(); private object _myLock = new object(); - public void Send(Message message, string id = null) + public BroadcasterService(ILogger logger) + { + _logger = logger; + } + + public void Send(Message message) { lock (_myLock) { - if (!string.IsNullOrWhiteSpace(id)) - { - if (_subscribers.ContainsKey(id)) - { - Task.Run(() => _subscribers[id].Invoke(message)); - } - return; - } foreach (var sub in _subscribers) { - Task.Run(() => sub.Value.Invoke(message)); + Task.Run(() => + { + try + { + sub.Value(message); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + }); + } + } + } + + public void Send(Message message, string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return; + } + + lock (_myLock) + { + if (_subscribers.TryGetValue(id, out var action)) + { + Task.Run(() => + { + try + { + action(message); + } + catch (Exception ex) + { + _logger.Exception(ex); + } + }); } } } @@ -34,14 +68,7 @@ namespace Bit.App.Services { lock (_myLock) { - if (_subscribers.ContainsKey(id)) - { - _subscribers[id] = messageCallback; - } - else - { - _subscribers.Add(id, messageCallback); - } + _subscribers[id] = messageCallback; } } diff --git a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs index a8904ef6b..60e59ba23 100644 --- a/src/iOS.Core/Renderers/CustomTabbedRenderer.cs +++ b/src/iOS.Core/Renderers/CustomTabbedRenderer.cs @@ -21,7 +21,7 @@ namespace Bit.iOS.Core.Renderers public CustomTabbedRenderer() { _broadcasterService = ServiceContainer.Resolve("broadcasterService"); - _broadcasterService.Subscribe(nameof(CustomTabbedRenderer), async (message) => + _broadcasterService.Subscribe(nameof(CustomTabbedRenderer), (message) => { if (message.Command == "updatedTheme") { diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index cfd0bf1bb..21321803e 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -38,13 +38,15 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Register("nativeLogService", new ConsoleLogService()); } + ILogger logger = null; if (ServiceContainer.Resolve("logger", true) == null) { #if DEBUG - ServiceContainer.Register("logger", DebugLogger.Instance); + logger = DebugLogger.Instance; #else - ServiceContainer.Register("logger", Logger.Instance); + logger = Logger.Instance; #endif + ServiceContainer.Register("logger", logger); } var preferencesStorage = new PreferencesStorageService(AppGroupId); @@ -52,7 +54,7 @@ namespace Bit.iOS.Core.Utilities var liteDbStorage = new LiteDbStorageService( Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); var localizeService = new LocalizeService(); - var broadcasterService = new BroadcasterService(); + var broadcasterService = new BroadcasterService(logger); var messagingService = new MobileBroadcasterMessagingService(broadcasterService); var i18nService = new MobileI18nService(localizeService.GetCurrentCultureInfo()); var secureStorageService = new KeyChainStorageService(AppId, AccessGroup, diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 020a3803b..9686c8071 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -59,114 +59,121 @@ namespace Bit.iOS _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => { - if (message.Command == "startEventTimer") + try { - StartEventTimer(); - } - else if (message.Command == "stopEventTimer") - { - var task = StopEventTimerAsync(); - } - else if (message.Command == "updatedTheme") - { - Device.BeginInvokeOnMainThread(() => + if (message.Command == "startEventTimer") { - iOSCoreHelpers.AppearanceAdjustments(); - }); - } - else if (message.Command == "listenYubiKeyOTP") - { - iOSCoreHelpers.ListenYubiKey((bool)message.Data, _deviceActionService, _nfcSession, _nfcDelegate); - } - else if (message.Command == "unlocked") - { - var needsAutofillReplacement = await _storageService.GetAsync( - Core.Constants.AutofillNeedsIdentityReplacementKey); - if (needsAutofillReplacement.GetValueOrDefault()) + StartEventTimer(); + } + else if (message.Command == "stopEventTimer") + { + var task = StopEventTimerAsync(); + } + else if (message.Command == "updatedTheme") + { + Device.BeginInvokeOnMainThread(() => + { + iOSCoreHelpers.AppearanceAdjustments(); + }); + } + else if (message.Command == "listenYubiKeyOTP") + { + iOSCoreHelpers.ListenYubiKey((bool)message.Data, _deviceActionService, _nfcSession, _nfcDelegate); + } + else if (message.Command == "unlocked") + { + var needsAutofillReplacement = await _storageService.GetAsync( + Core.Constants.AutofillNeedsIdentityReplacementKey); + if (needsAutofillReplacement.GetValueOrDefault()) + { + await ASHelpers.ReplaceAllIdentities(); + } + } + else if (message.Command == "showAppExtension") + { + Device.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); + } + else if (message.Command == "syncCompleted") + { + if (message.Data is Dictionary data && data.ContainsKey("successfully")) + { + var success = data["successfully"] as bool?; + if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + { + await ASHelpers.ReplaceAllIdentities(); + } + } + } + else if (message.Command == "addedCipher" || message.Command == "editedCipher" || + message.Command == "restoredCipher") + { + if (_deviceActionService.SystemMajorVersion() >= 12) + { + if (await ASHelpers.IdentitiesCanIncremental()) + { + var cipherId = message.Data as string; + if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId)) + { + var identity = await ASHelpers.GetCipherIdentityAsync(cipherId); + if (identity == null) + { + return; + } + await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync( + new ASPasswordCredentialIdentity[] { identity }); + return; + } + } + await ASHelpers.ReplaceAllIdentities(); + } + } + else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") + { + if (_deviceActionService.SystemMajorVersion() >= 12) + { + if (await ASHelpers.IdentitiesCanIncremental()) + { + var identity = ASHelpers.ToCredentialIdentity( + message.Data as Bit.Core.Models.View.CipherView); + if (identity == null) + { + return; + } + await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync( + new ASPasswordCredentialIdentity[] { identity }); + return; + } + await ASHelpers.ReplaceAllIdentities(); + } + } + else if (message.Command == "logout") + { + if (_deviceActionService.SystemMajorVersion() >= 12) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } + } + else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") + && _deviceActionService.SystemMajorVersion() >= 12) { await ASHelpers.ReplaceAllIdentities(); } - } - else if (message.Command == "showAppExtension") - { - Device.BeginInvokeOnMainThread(() => ShowAppExtension((ExtensionPageViewModel)message.Data)); - } - else if (message.Command == "syncCompleted") - { - if (message.Data is Dictionary data && data.ContainsKey("successfully")) + else if (message.Command == "vaultTimeoutActionChanged") { - var success = data["successfully"] as bool?; - if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12) + var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } + else { await ASHelpers.ReplaceAllIdentities(); } } } - else if (message.Command == "addedCipher" || message.Command == "editedCipher" || - message.Command == "restoredCipher") + catch (Exception ex) { - if (_deviceActionService.SystemMajorVersion() >= 12) - { - if (await ASHelpers.IdentitiesCanIncremental()) - { - var cipherId = message.Data as string; - if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId)) - { - var identity = await ASHelpers.GetCipherIdentityAsync(cipherId); - if (identity == null) - { - return; - } - await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync( - new ASPasswordCredentialIdentity[] { identity }); - return; - } - } - await ASHelpers.ReplaceAllIdentities(); - } - } - else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher") - { - if (_deviceActionService.SystemMajorVersion() >= 12) - { - if (await ASHelpers.IdentitiesCanIncremental()) - { - var identity = ASHelpers.ToCredentialIdentity( - message.Data as Bit.Core.Models.View.CipherView); - if (identity == null) - { - return; - } - await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync( - new ASPasswordCredentialIdentity[] { identity }); - return; - } - await ASHelpers.ReplaceAllIdentities(); - } - } - else if (message.Command == "logout") - { - if (_deviceActionService.SystemMajorVersion() >= 12) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - } - } - else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher") - && _deviceActionService.SystemMajorVersion() >= 12) - { - await ASHelpers.ReplaceAllIdentities(); - } - else if (message.Command == "vaultTimeoutActionChanged") - { - var timeoutAction = await _stateService.GetVaultTimeoutActionAsync(); - if (timeoutAction == VaultTimeoutAction.Logout) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - } - else - { - await ASHelpers.ReplaceAllIdentities(); - } + LoggerHelper.LogEvenIfCantBeResolved(ex); } }); From e2502e2e0c4f6a0012430c9f7cc2e0b577909202 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 5 Jul 2022 18:14:31 -0300 Subject: [PATCH 03/23] Improved the ServiceContainer to be easier to use and not to have the service name hardcoded to register/resolve a service (#1865) --- src/Core/Utilities/ServiceContainer.cs | 92 ++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 19450173b..84dda33bd 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Services; @@ -8,7 +10,7 @@ namespace Bit.Core.Utilities { public static class ServiceContainer { - public static Dictionary RegisteredServices { get; set; } = new Dictionary(); + public static ConcurrentDictionary RegisteredServices { get; set; } = new ConcurrentDictionary(); public static bool Inited { get; set; } public static void Init(string customUserAgent = null, string clearCipherCacheKey = null, @@ -109,18 +111,17 @@ namespace Bit.Core.Utilities public static void Register(string serviceName, T obj) { - if (RegisteredServices.ContainsKey(serviceName)) + if (!RegisteredServices.TryAdd(serviceName, obj)) { throw new Exception($"Service {serviceName} has already been registered."); } - RegisteredServices.Add(serviceName, obj); } public static T Resolve(string serviceName, bool dontThrow = false) { - if (RegisteredServices.ContainsKey(serviceName)) + if (RegisteredServices.TryGetValue(serviceName, out var service)) { - return (T)RegisteredServices[serviceName]; + return (T)service; } if (dontThrow) { @@ -129,6 +130,59 @@ namespace Bit.Core.Utilities throw new Exception($"Service {serviceName} is not registered."); } + public static void Register(T obj) + where T : class + { + Register(typeof(T), obj); + } + + public static void Register(Type type, object obj) + { + var serviceName = GetServiceRegistrationName(type); + if (!RegisteredServices.TryAdd(serviceName, obj)) + { + throw new Exception($"Service {serviceName} has already been registered."); + } + } + + public static T Resolve() + where T : class + { + return (T)Resolve(typeof(T)); + } + + public static object Resolve(Type type) + { + var serviceName = GetServiceRegistrationName(type); + if (RegisteredServices.TryGetValue(serviceName, out var service)) + { + return service; + } + throw new Exception($"Service {serviceName} is not registered."); + } + + public static bool TryResolve(out T service) + where T : class + { + try + { + var toReturn = TryResolve(typeof(T), out var serviceObj); + service = (T)serviceObj; + return toReturn; + } + catch (Exception) + { + service = null; + return false; + } + } + + public static bool TryResolve(Type type, out object service) + { + var serviceName = GetServiceRegistrationName(type); + return RegisteredServices.TryGetValue(serviceName, out service); + } + public static void Reset() { foreach (var service in RegisteredServices) @@ -140,7 +194,33 @@ namespace Bit.Core.Utilities } Inited = false; RegisteredServices.Clear(); - RegisteredServices = new Dictionary(); + RegisteredServices = new ConcurrentDictionary(); + } + + /// + /// Gets the service registration name + /// + /// Type of the service + /// + /// In order to work with already register/resolve we need to maintain the naming convention + /// of camelCase without the first "I" on the services interfaces + /// e.g. "ITokenService" -> "tokenService" + /// + static string GetServiceRegistrationName(Type type) + { + var typeName = type.Name; + var sb = new StringBuilder(); + + var indexToLowerCase = 0; + if (typeName[0] == 'I' && char.IsUpper(typeName[1])) + { + // if it's an interface then we ignore the first char + // and lower case the 2nd one (index 1) + indexToLowerCase = 1; + } + sb.Append(char.ToLower(typeName[indexToLowerCase])); + sb.Append(typeName.Substring(++indexToLowerCase)); + return sb.ToString(); } } } From d246d1dece1608dd94b47badd3256a080162a02b Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 5 Jul 2022 18:14:46 -0300 Subject: [PATCH 04/23] EC-297 Fix possible crash when copying password on cipher item view. Also improved a bit the code of copying commands (#1974) --- src/Android/Services/ClipboardService.cs | 26 ++++++++++++++-------- src/App/Pages/Vault/ViewPageViewModel.cs | 28 ++++++++++-------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/Android/Services/ClipboardService.cs b/src/Android/Services/ClipboardService.cs index 2abd8df45..7978e8523 100644 --- a/src/Android/Services/ClipboardService.cs +++ b/src/Android/Services/ClipboardService.cs @@ -28,17 +28,25 @@ namespace Bit.Droid.Services public async Task CopyTextAsync(string text, int expiresInMs = -1, bool isSensitive = true) { - // Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+ - if ((int)Build.VERSION.SdkInt < 33) + try { - await Clipboard.SetTextAsync(text); - } - else - { - CopyToClipboard(text, isSensitive); - } + // Xamarin.Essentials.Clipboard currently doesn't support the IS_SENSITIVE flag for API 33+ + if ((int)Build.VERSION.SdkInt < 33) + { + await Clipboard.SetTextAsync(text); + } + else + { + CopyToClipboard(text, isSensitive); + } - await ClearClipboardAlarmAsync(expiresInMs); + await ClearClipboardAlarmAsync(expiresInMs); + } + catch (Java.Lang.SecurityException ex) when (ex.Message.Contains("does not belong to")) + { + // #1962 Just ignore, the content is copied either way but there is some app interfiering in the process + // that the OS catches and just throws this exception. + } } public bool IsCopyNotificationHandledByPlatform() diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index 505fdb00b..17f8ef4f6 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Windows.Input; using Bit.App.Abstractions; using Bit.App.Resources; using Bit.App.Utilities; @@ -11,6 +12,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Bit.App.Pages @@ -28,6 +30,7 @@ namespace Bit.App.Pages private readonly IPasswordRepromptService _passwordRepromptService; private readonly ILocalizeService _localizeService; private readonly IClipboardService _clipboardService; + private readonly ILogger _logger; private CipherView _cipher; private List _fields; @@ -58,10 +61,11 @@ namespace Bit.App.Pages _passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); _localizeService = ServiceContainer.Resolve("localizeService"); _clipboardService = ServiceContainer.Resolve("clipboardService"); + _logger = ServiceContainer.Resolve("logger"); - CopyCommand = new Command((id) => CopyAsync(id, null)); - CopyUriCommand = new Command(CopyUri); - CopyFieldCommand = new Command(CopyField); + CopyCommand = new AsyncCommand((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); + CopyUriCommand = new AsyncCommand(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); + CopyFieldCommand = new AsyncCommand(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); LaunchUriCommand = new Command(LaunchUri); TogglePasswordCommand = new Command(TogglePassword); ToggleCardNumberCommand = new Command(ToggleCardNumber); @@ -72,9 +76,9 @@ namespace Bit.App.Pages PageTitle = AppResources.ViewItem; } - public Command CopyCommand { get; set; } - public Command CopyUriCommand { get; set; } - public Command CopyFieldCommand { get; set; } + public ICommand CopyCommand { get; set; } + public ICommand CopyUriCommand { get; set; } + public ICommand CopyFieldCommand { get; set; } public Command LaunchUriCommand { get; set; } public Command TogglePasswordCommand { get; set; } public Command ToggleCardNumberCommand { get; set; } @@ -616,7 +620,7 @@ namespace Bit.App.Pages _attachmentFilename = null; } - private async void CopyAsync(string id, string text = null) + private async Task CopyAsync(string id, string text = null) { if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync()) { @@ -680,16 +684,6 @@ namespace Bit.App.Pages } } - private void CopyUri(LoginUriView uri) - { - CopyAsync("LoginUri", uri.Uri); - } - - private void CopyField(FieldView field) - { - CopyAsync(field.Type == Core.Enums.FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value); - } - private void LaunchUri(LoginUriView uri) { if (uri.CanLaunch && (Page as BaseContentPage).DoOnce()) From 547e61a66bd8123e2f11e9fa07d96a3bd61766f7 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 5 Jul 2022 18:44:45 -0300 Subject: [PATCH 05/23] Fix formatting (#1975) --- src/App/Pages/Accounts/LockPageViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index 0b771ae1e..5419916e9 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -349,7 +349,7 @@ namespace Bit.App.Pages _messagingService.Send("logout"); } } - + public void TogglePassword() { ShowPassword = !ShowPassword; From 6c7413e38ce63def791e1909dbbc55cafd356c04 Mon Sep 17 00:00:00 2001 From: Ben Pearo <35578281+BenPearo@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:43:07 -0400 Subject: [PATCH 06/23] replace link to mobile section of contributing documentation with working link (#1978) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b03ac86d9..3a268ac0b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The Bitwarden mobile application is written in C# with Xamarin Android, Xamarin # Build/Run -Please refer to the [Mobile section](https://contributing.bitwarden.com/clients/mobile) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. +Please refer to the [Mobile section](https://contributing.bitwarden.com/mobile/) of the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started. # We're Hiring! From 58a3662d0faad97d081d680d9769fef762a60b57 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 6 Jul 2022 18:23:20 -0400 Subject: [PATCH 07/23] Add user verification to reset password request (#1980) We only need master password hash because this is currently only used for sso password setting after auto-provisioning. Key Connector is not involved in these accounts --- src/App/Pages/Accounts/SetPasswordPageViewModel.cs | 3 ++- .../Request/OrganizationUserResetPasswordEnrollmentRequest.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs index fd4adfd05..b4c415959 100644 --- a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs +++ b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs @@ -219,7 +219,8 @@ namespace Bit.App.Pages // Request var resetRequest = new OrganizationUserResetPasswordEnrollmentRequest { - ResetPasswordKey = encryptedKey.EncryptedString + ResetPasswordKey = encryptedKey.EncryptedString, + MasterPasswordHash = masterPasswordHash, }; var userId = await _stateService.GetActiveUserIdAsync(); // Enroll user diff --git a/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs index 267a425ec..663e0fc61 100644 --- a/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs +++ b/src/Core/Models/Request/OrganizationUserResetPasswordEnrollmentRequest.cs @@ -2,6 +2,7 @@ { public class OrganizationUserResetPasswordEnrollmentRequest { + public string MasterPasswordHash { get; set; } public string ResetPasswordKey { get; set; } } } From cd56a124d5cde287f7f13a2b23090de6c5860a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 7 Jul 2022 16:35:58 +0100 Subject: [PATCH 08/23] [EC-303] Add warning when vault timeout is set to "Never" (#1976) * Added to AppResources.resx the message warning the user about setting the lockout option to "Never" * Added the condition to check the newly selected option on the vault timeout settings * Changed the wording on the warning as to reflect the mobile version * Changed the vault timeout modal to have the ability to cancel * Simplified the reversion of value if the user cancels the change --- .../Settings/SettingsPage/SettingsPageViewModel.cs | 11 +++++++++++ src/App/Resources/AppResources.Designer.cs | 8 ++++++++ src/App/Resources/AppResources.resx | 3 +++ 3 files changed, 22 insertions(+) diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs index 48a4e375a..831be4979 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs @@ -257,6 +257,17 @@ namespace Bit.App.Pages } var cleanSelection = selection.Replace("✓ ", string.Empty); var selectionOption = _vaultTimeouts.FirstOrDefault(o => o.Key == cleanSelection); + + // Check if the selected Timeout action is "Never" and if it's different from the previous selected value + if (selectionOption.Value == null && selectionOption.Value != oldTimeout) + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning, + AppResources.Warning, AppResources.Yes, AppResources.Cancel); + if (!confirmed) + { + return; + } + } _vaultTimeoutDisplayValue = selectionOption.Key; newTimeout = selectionOption.Value; } diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 86794091b..3e1ac1c0a 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -4034,5 +4034,13 @@ namespace Bit.App.Resources { return ResourceManager.GetString("All", resourceCulture); } } + + public static string NeverLockWarning + { + get + { + return ResourceManager.GetString("NeverLockWarning", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index ee27902e0..4417d5b09 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2254,4 +2254,7 @@ All + + Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected. + From 7802da2b9c488023a99ad81399b0e73234f67179 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 7 Jul 2022 09:07:08 -0700 Subject: [PATCH 09/23] Bumped version to 2022.6.2 (#1981) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/Android/Properties/AndroidManifest.xml | 2 +- src/iOS.Autofill/Info.plist | 2 +- src/iOS.Extension/Info.plist | 2 +- src/iOS.ShareExtension/Info.plist | 2 +- src/iOS/Info.plist | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml index f04fd090a..6ccf42d66 100644 --- a/src/Android/Properties/AndroidManifest.xml +++ b/src/Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@ - + diff --git a/src/iOS.Autofill/Info.plist b/src/iOS.Autofill/Info.plist index d467245f3..883357ee9 100644 --- a/src/iOS.Autofill/Info.plist +++ b/src/iOS.Autofill/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden.autofill CFBundleShortVersionString - 2022.6.1 + 2022.6.2 CFBundleVersion 1 CFBundleLocalizations diff --git a/src/iOS.Extension/Info.plist b/src/iOS.Extension/Info.plist index 7ef132a1a..31083a956 100644 --- a/src/iOS.Extension/Info.plist +++ b/src/iOS.Extension/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden.find-login-action-extension CFBundleShortVersionString - 2022.6.1 + 2022.6.2 CFBundleLocalizations en diff --git a/src/iOS.ShareExtension/Info.plist b/src/iOS.ShareExtension/Info.plist index 64aee5209..6277983db 100644 --- a/src/iOS.ShareExtension/Info.plist +++ b/src/iOS.ShareExtension/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2022.6.1 + 2022.6.2 CFBundleVersion 1 MinimumOSVersion diff --git a/src/iOS/Info.plist b/src/iOS/Info.plist index 9bf586efb..e3ddbb167 100644 --- a/src/iOS/Info.plist +++ b/src/iOS/Info.plist @@ -11,7 +11,7 @@ CFBundleIdentifier com.8bit.bitwarden CFBundleShortVersionString - 2022.6.1 + 2022.6.2 CFBundleVersion 1 CFBundleIconName From 846d3a85a27d3e9d99d037173f58e76c16425819 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 7 Jul 2022 16:24:29 -0300 Subject: [PATCH 10/23] EC-308 Fix crash produced by creating avatar image on AccountSwitchingOverlayHelper and also added more logging to see when it happens. (#1983) --- .../AccountSwitchingOverlayHelper.cs | 24 +++++++++++++++---- .../Utilities/ImageSourceExtensions.cs | 12 ++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs index 8812285bb..4cde4b941 100644 --- a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs +++ b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Bit.App.Controls; using Bit.Core.Abstractions; using Bit.Core.Utilities; @@ -10,6 +11,8 @@ namespace Bit.iOS.Core.Utilities { public class AccountSwitchingOverlayHelper { + const string DEFAULT_SYSTEM_AVATAR_IMAGE = "person.2"; + IStateService _stateService; IMessagingService _messagingService; ILogger _logger; @@ -23,9 +26,22 @@ namespace Bit.iOS.Core.Utilities public async Task CreateAvatarImageAsync() { - var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); - var avatarUIImage = await avatarImageSource.GetNativeImageAsync(); - return avatarUIImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); + try + { + if (_stateService is null) + { + throw new NullReferenceException(nameof(_stateService)); + } + + var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); + var avatarUIImage = await avatarImageSource.GetNativeImageAsync(); + return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + } + catch (Exception ex) + { + _logger.Exception(ex); + return UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + } } public AccountSwitchingOverlayView CreateAccountSwitchingOverlayView(UIView containerView) diff --git a/src/iOS.Core/Utilities/ImageSourceExtensions.cs b/src/iOS.Core/Utilities/ImageSourceExtensions.cs index 25941a0bd..7fcf545cc 100644 --- a/src/iOS.Core/Utilities/ImageSourceExtensions.cs +++ b/src/iOS.Core/Utilities/ImageSourceExtensions.cs @@ -1,9 +1,9 @@ using System; using System.Threading; using System.Threading.Tasks; +using Bit.Core.Services; using UIKit; using Xamarin.Forms; -using Xamarin.Forms.Internals; using Xamarin.Forms.Platform.iOS; namespace Bit.iOS.Core.Utilities @@ -17,25 +17,29 @@ namespace Bit.iOS.Core.Utilities public static async Task GetNativeImageAsync(this ImageSource source, CancellationToken cancellationToken = default(CancellationToken)) { if (source == null || source.IsEmpty) + { return null; + } var handler = Xamarin.Forms.Internals.Registrar.Registered.GetHandlerForObject(source); if (handler == null) + { + LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed cause IImageSourceHandler couldn't be found")); return null; + } try { float scale = (float)UIScreen.MainScreen.Scale; - return await handler.LoadImageAsync(source, scale: scale, cancelationToken: cancellationToken); } catch (OperationCanceledException) { - Log.Warning("Image loading", "Image load cancelled"); + LoggerHelper.LogEvenIfCantBeResolved(new OperationCanceledException("GetNativeImageAsync was cancelled")); } catch (Exception ex) { - Log.Warning("Image loading", $"Image load failed: {ex}"); + LoggerHelper.LogEvenIfCantBeResolved(new InvalidOperationException("GetNativeImageAsync failed", ex)); } return null; From cceded2a0ff6519289791843ba1dbb2bc532ddc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 8 Jul 2022 09:28:23 +0100 Subject: [PATCH 11/23] Updated the wording on the modal warning when deleting the account (#1982) --- src/App/Resources/AppResources.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 4417d5b09..8cd72b63f 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2165,7 +2165,7 @@ Deleting your account is permanent - Your account and all associated data will be erased and unrecoverable. Are you sure you want to continue? + Your account and all vault data will be erased and unrecoverable. Are you sure you want to continue? Deleting your account From 67f49a05915fa2101b8ecd9a250b205c4ab3e4ac Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 11 Jul 2022 08:45:42 +0200 Subject: [PATCH 12/23] [PS-686] Mobile update negative copy in settings (#1961) * feat: update auto totp copy setting * feat: update show icons settings * feat: update auto add settings * feat: update settings and options to sentence case * feat: update translation keys With the latest changes the translation keys had diverged from their contents. This commit fixes that. * fix: revert AndroidManifest changes * chore: add todo comments to fix negative functions --- src/App/Pages/Settings/OptionsPage.xaml | 18 +++--- .../Pages/Settings/OptionsPageViewModel.cs | 49 ++++++++-------- src/App/Resources/AppResources.Designer.cs | 28 ++++----- src/App/Resources/AppResources.resx | 58 +++++++++---------- src/Core/Abstractions/ITotpService.cs | 1 - src/Core/Services/TotpService.cs | 9 --- src/Core/Utilities/ServiceContainer.cs | 2 +- 7 files changed, 79 insertions(+), 86 deletions(-) diff --git a/src/App/Pages/Settings/OptionsPage.xaml b/src/App/Pages/Settings/OptionsPage.xaml index 8d37074c6..8c4e3081b 100644 --- a/src/App/Pages/Settings/OptionsPage.xaml +++ b/src/App/Pages/Settings/OptionsPage.xaml @@ -83,31 +83,31 @@ @@ -117,16 +117,16 @@ - Help and Feedback + Help and feedback Hide @@ -269,7 +269,7 @@ Title for login page. (noun) - Log Out + Log out The log out button text (verb). @@ -412,7 +412,7 @@ Add an Item - App Extension + App extension Use the Bitwarden accessibility service to auto-fill your logins across apps and the web. @@ -516,7 +516,7 @@ Get your master password hint - Import Items + Import items You can bulk import items from the bitwarden.com web vault. Do you want to visit the website now? @@ -549,10 +549,10 @@ Immediately - Vault Timeout + Vault timeout - Vault Timeout Action + Vault timeout action Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting? @@ -647,7 +647,7 @@ Push notifications for apple products - Rate the App + Rate the app Please consider helping us out with a good review! @@ -701,7 +701,7 @@ What Apple calls their fingerprint reader. - Two-step Login + Two-step login Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now? @@ -710,7 +710,7 @@ Unlock with {0} - Unlock with PIN Code + Unlock with PIN code Validating @@ -907,11 +907,11 @@ Copy TOTP - - If your login has an authenticator key attached to it, the TOTP verification code is automatically copied to your clipboard whenever you auto-fill the login. + + If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login. - - Disable Automatic TOTP Copy + + Copy TOTP automatically A premium membership is required to use this feature. @@ -1128,11 +1128,11 @@ Expiration - - Disable Website Icons + + Show website icons - - Website Icons provide a recognizable image next to each login item in your vault. + + Show a recognizable image next to each login. Icons Server URL @@ -1311,7 +1311,7 @@ 5. Select "Bitwarden" - Password AutoFill + Password auto-fill The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Settings" screen. @@ -1449,7 +1449,7 @@ There are no folders to list. - Fingerprint Phrase + Fingerprint phrase A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing. @@ -1460,10 +1460,10 @@ Bitwarden allows you to share your vault items with others by using an organization account. Would you like to visit the bitwarden.com website to learn more? - Export Vault + Export vault - Lock Now + Lock now PIN @@ -1517,7 +1517,7 @@ 2 minutes - Clear Clipboard + Clear clipboard Clipboard is the operating system thing where you copy/paste data to on your device. @@ -1525,7 +1525,7 @@ Clipboard is the operating system thing where you copy/paste data to on your device. - Default URI Match Detection + Default URI match detection Default URI match detection for auto-fill. @@ -1573,14 +1573,14 @@ URIs that are blacklisted will not offer auto-fill. The list should be comma separated. Ex: "https://twitter.com, androidapp://com.twitter.android". - - Disable Save Prompt + + Ask to add login - - The "Save Prompt" automatically prompts you to save new items to your vault whenever you enter them for the first time. + + Ask to add an item if one isn't found in your vault. - On App Restart + On app restart Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen. @@ -2159,7 +2159,7 @@ Account removed successfully - Delete Account + Delete account Deleting your account is permanent diff --git a/src/Core/Abstractions/ITotpService.cs b/src/Core/Abstractions/ITotpService.cs index e0aff3c2f..717791b3a 100644 --- a/src/Core/Abstractions/ITotpService.cs +++ b/src/Core/Abstractions/ITotpService.cs @@ -6,6 +6,5 @@ namespace Bit.Core.Abstractions { Task GetCodeAsync(string key); int GetTimeInterval(string key); - Task IsAutoCopyEnabledAsync(); } } diff --git a/src/Core/Services/TotpService.cs b/src/Core/Services/TotpService.cs index df74920d7..358dfeb51 100644 --- a/src/Core/Services/TotpService.cs +++ b/src/Core/Services/TotpService.cs @@ -10,14 +10,11 @@ namespace Bit.Core.Services { private const string SteamChars = "23456789BCDFGHJKMNPQRTVWXY"; - private readonly IStateService _stateService; private readonly ICryptoFunctionService _cryptoFunctionService; public TotpService( - IStateService stateService, ICryptoFunctionService cryptoFunctionService) { - _stateService = stateService; _cryptoFunctionService = cryptoFunctionService; } @@ -132,11 +129,5 @@ namespace Bit.Core.Services } return period; } - - public async Task IsAutoCopyEnabledAsync() - { - var disabled = await _stateService.GetDisableAutoTotpCopyAsync(); - return !disabled.GetValueOrDefault(); - } } } diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 84dda33bd..dedb7e551 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -74,7 +74,7 @@ namespace Bit.Core.Utilities }); var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService); - var totpService = new TotpService(stateService, cryptoFunctionService); + var totpService = new TotpService(cryptoFunctionService); var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService, keyConnectorService); From 75e827678425497315cbf4eb5609fbc3f6941894 Mon Sep 17 00:00:00 2001 From: "Patrick H. Lauke" Date: Mon, 11 Jul 2022 14:48:19 +0100 Subject: [PATCH 13/23] Use correct icon for checked/unchecked boolean (#1986) Closes https://github.com/bitwarden/mobile/issues/1985 --- src/App/Pages/Vault/ViewPageViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index 17f8ef4f6..8934d39f0 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -750,7 +750,7 @@ namespace Bit.App.Pages { if (IsBooleanType) { - return _field.Value == "true" ? BitwardenIcons.Square : BitwardenIcons.CheckSquare; + return _field.Value == "true" ? BitwardenIcons.CheckSquare : BitwardenIcons.Square; } else if (IsLinkedType) { From d621a5d2f32454f206cc09937e484e823dab05f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Gon=C3=A7alves?= Date: Mon, 11 Jul 2022 18:02:11 +0100 Subject: [PATCH 14/23] [PS 920] Fix selfhosted url validations (#1967) * PS-920 - Added feedback to user when saving bad formed URLs * Added feedback to user when trying to perform login with bad formed URL * PS-920 - Refactor to use AsyncCommand *(missing file from previous commit) * PS-920 - Fixed whitespace formatting * PS-920 - Removed unused method * PS-920 - Fixed validation * Added comment for hard coded string * PS-920 - Removed unused properties * Fixed url validations * Refactored method to local function * PS-920 - Added exception handling and logging * Added generic error message string to AppResources --- src/App/Pages/Accounts/EnvironmentPage.xaml | 2 +- .../Pages/Accounts/EnvironmentPage.xaml.cs | 8 ----- .../Accounts/EnvironmentPageViewModel.cs | 34 +++++++++++++++++-- src/App/Resources/AppResources.Designer.cs | 14 +++++++- src/App/Resources/AppResources.resx | 8 ++++- src/Core/Services/ApiService.cs | 12 +++++++ 6 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/App/Pages/Accounts/EnvironmentPage.xaml b/src/App/Pages/Accounts/EnvironmentPage.xaml index 5a7abb783..cabf07ed7 100644 --- a/src/App/Pages/Accounts/EnvironmentPage.xaml +++ b/src/App/Pages/Accounts/EnvironmentPage.xaml @@ -14,7 +14,7 @@ - + diff --git a/src/App/Pages/Accounts/EnvironmentPage.xaml.cs b/src/App/Pages/Accounts/EnvironmentPage.xaml.cs index b57d2f440..baa2dd1c0 100644 --- a/src/App/Pages/Accounts/EnvironmentPage.xaml.cs +++ b/src/App/Pages/Accounts/EnvironmentPage.xaml.cs @@ -36,14 +36,6 @@ namespace Bit.App.Pages }; } - private async void Submit_Clicked(object sender, EventArgs e) - { - if (DoOnce()) - { - await _vm.SubmitAsync(); - } - } - private async Task SubmitSuccessAsync() { _platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved); diff --git a/src/App/Pages/Accounts/EnvironmentPageViewModel.cs b/src/App/Pages/Accounts/EnvironmentPageViewModel.cs index b25113c41..07ce99a45 100644 --- a/src/App/Pages/Accounts/EnvironmentPageViewModel.cs +++ b/src/App/Pages/Accounts/EnvironmentPageViewModel.cs @@ -1,15 +1,17 @@ using System; using System.Threading.Tasks; +using System.Windows.Input; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Utilities; -using Xamarin.Forms; +using Xamarin.CommunityToolkit.ObjectModel; namespace Bit.App.Pages { public class EnvironmentPageViewModel : BaseViewModel { private readonly IEnvironmentService _environmentService; + readonly LazyResolve _logger = new LazyResolve("logger"); public EnvironmentPageViewModel() { @@ -22,10 +24,10 @@ namespace Bit.App.Pages IdentityUrl = _environmentService.IdentityUrl; IconsUrl = _environmentService.IconsUrl; NotificationsUrls = _environmentService.NotificationsUrl; - SubmitCommand = new Command(async () => await SubmitAsync()); + SubmitCommand = new AsyncCommand(SubmitAsync, onException: ex => OnSubmitException(ex), allowsMultipleExecutions: false); } - public Command SubmitCommand { get; } + public ICommand SubmitCommand { get; } public string BaseUrl { get; set; } public string ApiUrl { get; set; } public string IdentityUrl { get; set; } @@ -37,6 +39,12 @@ namespace Bit.App.Pages public async Task SubmitAsync() { + if (!ValidateUrls()) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.EnvironmentPageUrlsError, AppResources.Ok); + return; + } + var resUrls = await _environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData { Base = BaseUrl, @@ -57,5 +65,25 @@ namespace Bit.App.Pages SubmitSuccessAction?.Invoke(); } + + public bool ValidateUrls() + { + bool IsUrlValid(string url) + { + return string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute); + } + + return IsUrlValid(BaseUrl) + && IsUrlValid(ApiUrl) + && IsUrlValid(IdentityUrl) + && IsUrlValid(WebVaultUrl) + && IsUrlValid(IconsUrl); + } + + private void OnSubmitException(Exception ex) + { + _logger.Value.Exception(ex); + Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok); + } } } diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index e25fcdcdb..09f2c3f98 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -4044,5 +4044,17 @@ namespace Bit.App.Resources { return ResourceManager.GetString("NeverLockWarning", resourceCulture); } } + + public static string EnvironmentPageUrlsError { + get { + return ResourceManager.GetString("EnvironmentPageUrlsError", resourceCulture); + } + } + + public static string GenericErrorMessage { + get { + return ResourceManager.GetString("GenericErrorMessage", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index a04488167..58cc1f7c9 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Send/SendAddOnlyPage.xaml.cs b/src/App/Pages/Send/SendAddOnlyPage.xaml.cs new file mode 100644 index 000000000..821c9f817 --- /dev/null +++ b/src/App/Pages/Send/SendAddOnlyPage.xaml.cs @@ -0,0 +1,178 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Models; +using Bit.App.Utilities; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + /// + /// This is a version of that is reduced for adding only and adapted + /// for performance for iOS Share extension. + /// + /// + /// This should NOT be used in Android. + /// + public partial class SendAddOnlyPage : BaseContentPage + { + private readonly IVaultTimeoutService _vaultTimeoutService; + private readonly LazyResolve _logger = new LazyResolve("logger"); + + private AppOptions _appOptions; + private SendAddEditPageViewModel _vm; + + public Action OnClose { get; set; } + public Action AfterSubmit { get; set; } + + public SendAddOnlyPage( + AppOptions appOptions = null, + string sendId = null, + SendType? type = null) + { + if (appOptions?.IosExtension != true) + { + throw new InvalidOperationException(nameof(SendAddOnlyPage) + " is only prepared to be used in iOS share extension"); + } + + _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); + _appOptions = appOptions; + InitializeComponent(); + _vm = BindingContext as SendAddEditPageViewModel; + _vm.Page = this; + _vm.SendId = sendId; + _vm.Type = appOptions?.CreateSend?.Item1 ?? type; + + if (_vm.IsText) + { + _nameEntry.ReturnType = ReturnType.Next; + _nameEntry.ReturnCommand = new Command(() => _textEditor.Focus()); + } + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + + try + { + if (!await AppHelpers.IsVaultTimeoutImmediateAsync()) + { + await _vaultTimeoutService.CheckVaultTimeoutAsync(); + } + if (await _vaultTimeoutService.IsLockedAsync()) + { + return; + } + await _vm.InitAsync(); + + if (!await _vm.LoadAsync()) + { + await CloseAsync(); + return; + } + + _accountAvatar?.OnAppearing(); + await Device.InvokeOnMainThreadAsync(async () => _vm.AvatarImageSource = await GetAvatarImageSourceAsync()); + + await HandleCreateRequest(); + if (string.IsNullOrWhiteSpace(_vm.Send?.Name)) + { + RequestFocus(_nameEntry); + } + AdjustToolbar(); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + await CloseAsync(); + } + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + _accountAvatar?.OnDisappearing(); + } + + private async Task CloseAsync() + { + if (OnClose is null) + { + await Navigation.PopModalAsync(); + } + else + { + OnClose(); + } + } + + private async void Save_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + var submitted = await _vm.SubmitAsync(); + if (submitted) + { + AfterSubmit?.Invoke(); + } + } + } + + private async void Close_Clicked(object sender, EventArgs e) + { + if (DoOnce()) + { + await CloseAsync(); + } + } + + private void AdjustToolbar() + { + _saveItem.IsEnabled = _vm.SendEnabled; + } + + private Task HandleCreateRequest() + { + if (_appOptions?.CreateSend == null) + { + return Task.CompletedTask; + } + + _vm.IsAddFromShare = true; + _vm.CopyInsteadOfShareAfterSaving = _appOptions.CopyInsteadOfShareAfterSaving; + + var name = _appOptions.CreateSend.Item2; + _vm.Send.Name = name; + + var type = _appOptions.CreateSend.Item1; + if (type == SendType.File) + { + _vm.FileData = _appOptions.CreateSend.Item3; + _vm.FileName = name; + } + else + { + var text = _appOptions.CreateSend.Item4; + _vm.Send.Text.Text = text; + _vm.TriggerSendTextPropertyChanged(); + } + _appOptions.CreateSend = null; + + return Task.CompletedTask; + } + + void OptionsHeader_Tapped(object sender, EventArgs e) + { + _vm.ToggleOptionsCommand.Execute(null); + + if (!_lazyOptionsView.IsLoaded) + { + _lazyOptionsView.MainScrollView = _scrollView; + _lazyOptionsView.LoadViewAsync(); + } + } + } +} diff --git a/src/App/Utilities/AccountManagement/AccountsManager.cs b/src/App/Utilities/AccountManagement/AccountsManager.cs index 1fac4b900..dd5b639f1 100644 --- a/src/App/Utilities/AccountManagement/AccountsManager.cs +++ b/src/App/Utilities/AccountManagement/AccountsManager.cs @@ -19,6 +19,7 @@ namespace Bit.App.Utilities.AccountManagement private readonly IStateService _stateService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IAuthService _authService; + private readonly ILogger _logger; Func _getOptionsFunc; private IAccountsManagerHost _accountsManagerHost; @@ -28,7 +29,8 @@ namespace Bit.App.Utilities.AccountManagement IStorageService secureStorageService, IStateService stateService, IPlatformUtilsService platformUtilsService, - IAuthService authService) + IAuthService authService, + ILogger logger) { _broadcasterService = broadcasterService; _vaultTimeoutService = vaultTimeoutService; @@ -36,6 +38,7 @@ namespace Bit.App.Utilities.AccountManagement _stateService = stateService; _platformUtilsService = platformUtilsService; _authService = authService; + _logger = logger; } private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true }; @@ -109,42 +112,45 @@ namespace Bit.App.Utilities.AccountManagement private async void OnMessage(Message message) { - switch (message.Command) + try { - case AccountsManagerMessageCommands.LOCKED: - Locked(message.Data as Tuple); - break; - case AccountsManagerMessageCommands.LOCK_VAULT: - await _vaultTimeoutService.LockAsync(true); - break; - case AccountsManagerMessageCommands.LOGOUT: - LogOut(message.Data as Tuple); - break; - case AccountsManagerMessageCommands.LOGGED_OUT: - // Clean up old migrated key if they ever log out. - await _secureStorageService.RemoveAsync("oldKey"); - break; - case AccountsManagerMessageCommands.ADD_ACCOUNT: - AddAccount(); - break; - case AccountsManagerMessageCommands.ACCOUNT_ADDED: - await _accountsManagerHost.UpdateThemeAsync(); - break; - case AccountsManagerMessageCommands.SWITCHED_ACCOUNT: - await SwitchedAccountAsync(); - break; + switch (message.Command) + { + case AccountsManagerMessageCommands.LOCKED: + await Device.InvokeOnMainThreadAsync(() => LockedAsync(message.Data as Tuple)); + break; + case AccountsManagerMessageCommands.LOCK_VAULT: + await _vaultTimeoutService.LockAsync(true); + break; + case AccountsManagerMessageCommands.LOGOUT: + await Device.InvokeOnMainThreadAsync(() => LogOutAsync(message.Data as Tuple)); + break; + case AccountsManagerMessageCommands.LOGGED_OUT: + // Clean up old migrated key if they ever log out. + await _secureStorageService.RemoveAsync("oldKey"); + break; + case AccountsManagerMessageCommands.ADD_ACCOUNT: + await AddAccountAsync(); + break; + case AccountsManagerMessageCommands.ACCOUNT_ADDED: + await _accountsManagerHost.UpdateThemeAsync(); + break; + case AccountsManagerMessageCommands.SWITCHED_ACCOUNT: + await SwitchedAccountAsync(); + break; + } + } + catch (Exception ex) + { + _logger.Exception(ex); } } - private void Locked(Tuple extras) + private async Task LockedAsync(Tuple extras) { var userId = extras?.Item1; var userInitiated = extras?.Item2 ?? false; - Device.BeginInvokeOnMainThread(async () => await LockedAsync(userId, userInitiated)); - } - private async Task LockedAsync(string userId, bool userInitiated) - { if (!await _stateService.IsActiveAccountAsync(userId)) { _platformUtilsService.ShowToast("info", null, AppResources.AccountLockedSuccessfully); @@ -163,28 +169,24 @@ namespace Bit.App.Utilities.AccountManagement await _accountsManagerHost.SetPreviousPageInfoAsync(); - Device.BeginInvokeOnMainThread(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric))); + await Device.InvokeOnMainThreadAsync(() => _accountsManagerHost.Navigate(NavigationTarget.Lock, new LockNavigationParams(autoPromptBiometric))); } - private void AddAccount() + private async Task AddAccountAsync() { - Device.BeginInvokeOnMainThread(() => + await Device.InvokeOnMainThreadAsync(() => { Options.HideAccountSwitcher = false; _accountsManagerHost.Navigate(NavigationTarget.HomeLogin); }); } - private void LogOut(Tuple extras) + private async Task LogOutAsync(Tuple extras) { var userId = extras?.Item1; var userInitiated = extras?.Item2 ?? true; var expired = extras?.Item3 ?? false; - Device.BeginInvokeOnMainThread(async () => await LogOutAsync(userId, userInitiated, expired)); - } - private async Task LogOutAsync(string userId, bool userInitiated, bool expired) - { await AppHelpers.LogOutAsync(userId, userInitiated); await NavigateOnAccountChangeAsync(); _authService.LogOut(() => diff --git a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs index 2dc1744c1..e4ad3248a 100644 --- a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs @@ -1,20 +1,20 @@ using System; -using UIKit; -using Foundation; -using Bit.iOS.Core.Views; -using Bit.App.Resources; -using Bit.iOS.Core.Utilities; -using Bit.App.Abstractions; -using Bit.Core.Abstractions; -using Bit.Core.Utilities; using System.Threading.Tasks; -using Bit.App.Utilities; -using Bit.Core.Models.Domain; -using Bit.Core.Enums; -using Bit.App.Pages; +using Bit.App.Abstractions; using Bit.App.Models; -using Xamarin.Forms; +using Bit.App.Pages; +using Bit.App.Resources; +using Bit.App.Utilities; using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using Bit.iOS.Core.Utilities; +using Bit.iOS.Core.Views; +using Foundation; +using UIKit; +using Xamarin.Forms; namespace Bit.iOS.Core.Controllers { @@ -39,6 +39,10 @@ namespace Bit.iOS.Core.Controllers protected bool autofillExtension = false; + public BaseLockPasswordViewController() + { + } + public BaseLockPasswordViewController(IntPtr handle) : base(handle) { } @@ -168,13 +172,12 @@ namespace Bit.iOS.Core.Controllers { TableView.BackgroundColor = ThemeHelpers.BackgroundColor; TableView.SeparatorColor = ThemeHelpers.SeparatorColor; + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 70; + TableView.Source = new TableSource(this); + TableView.AllowsSelection = true; } - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 70; - TableView.Source = new TableSource(this); - TableView.AllowsSelection = true; - base.ViewDidLoad(); if (_biometricLock) @@ -191,7 +194,7 @@ namespace Bit.iOS.Core.Controllers } } - public override async void ViewDidAppear(bool animated) + public override void ViewDidAppear(bool animated) { base.ViewDidAppear(animated); @@ -402,28 +405,43 @@ namespace Bit.iOS.Core.Controllers }); } + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + MasterPasswordCell?.Dispose(); + MasterPasswordCell = null; + + TableView?.Dispose(); + } + public class TableSource : ExtendedUITableViewSource { - private readonly BaseLockPasswordViewController _controller; + private readonly WeakReference _controller; public TableSource(BaseLockPasswordViewController controller) { - _controller = controller; + _controller = new WeakReference(controller); } public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) { + if (!_controller.TryGetTarget(out var controller)) + { + return new ExtendedUITableViewCell(); + } + if (indexPath.Section == 0) { if (indexPath.Row == 0) { - if (_controller._biometricUnlockOnly) + if (controller._biometricUnlockOnly) { - return _controller.BiometricCell; + return controller.BiometricCell; } else { - return _controller.MasterPasswordCell; + return controller.MasterPasswordCell; } } } @@ -431,7 +449,7 @@ namespace Bit.iOS.Core.Controllers { if (indexPath.Row == 0) { - if (_controller._passwordReprompt) + if (controller._passwordReprompt) { var cell = new ExtendedUITableViewCell(); cell.TextLabel.TextColor = ThemeHelpers.DangerColor; @@ -441,9 +459,9 @@ namespace Bit.iOS.Core.Controllers cell.TextLabel.Text = AppResources.PasswordConfirmationDesc; return cell; } - else if (!_controller._biometricUnlockOnly) + else if (!controller._biometricUnlockOnly) { - return _controller.BiometricCell; + return controller.BiometricCell; } } } @@ -457,8 +475,13 @@ namespace Bit.iOS.Core.Controllers public override nint NumberOfSections(UITableView tableView) { - return (!_controller._biometricUnlockOnly && _controller._biometricLock) || - _controller._passwordReprompt + if (!_controller.TryGetTarget(out var controller)) + { + return 0; + } + + return (!controller._biometricUnlockOnly && controller._biometricLock) || + controller._passwordReprompt ? 2 : 1; } @@ -484,13 +507,18 @@ namespace Bit.iOS.Core.Controllers public override void RowSelected(UITableView tableView, NSIndexPath indexPath) { + if (!_controller.TryGetTarget(out var controller)) + { + return; + } + tableView.DeselectRow(indexPath, true); tableView.EndEditing(true); if (indexPath.Row == 0 && - ((_controller._biometricUnlockOnly && indexPath.Section == 0) || + ((controller._biometricUnlockOnly && indexPath.Section == 0) || indexPath.Section == 1)) { - var task = _controller.PromptBiometricAsync(); + var task = controller.PromptBiometricAsync(); return; } var cell = tableView.CellAt(indexPath); diff --git a/src/iOS.Core/Controllers/ExtendedUIViewController.cs b/src/iOS.Core/Controllers/ExtendedUIViewController.cs index 8d0db352b..b599ac4e2 100644 --- a/src/iOS.Core/Controllers/ExtendedUIViewController.cs +++ b/src/iOS.Core/Controllers/ExtendedUIViewController.cs @@ -7,7 +7,11 @@ namespace Bit.iOS.Core.Controllers public class ExtendedUIViewController : UIViewController { public Action DismissModalAction { get; set; } - + + public ExtendedUIViewController() + { + } + public ExtendedUIViewController(IntPtr handle) : base(handle) { @@ -28,16 +32,28 @@ namespace Bit.iOS.Core.Controllers { View.BackgroundColor = ThemeHelpers.BackgroundColor; } - if (NavigationController?.NavigationBar != null) + UpdateNavigationBarTheme(); + } + + protected virtual void UpdateNavigationBarTheme() + { + UpdateNavigationBarTheme(NavigationController?.NavigationBar); + } + + protected void UpdateNavigationBarTheme(UINavigationBar navBar) + { + if (navBar is null) { - NavigationController.NavigationBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor; - NavigationController.NavigationBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor; - NavigationController.NavigationBar.TintColor = ThemeHelpers.NavBarTextColor; - NavigationController.NavigationBar.TitleTextAttributes = new UIStringAttributes - { - ForegroundColor = ThemeHelpers.NavBarTextColor - }; + return; } + + navBar.BarTintColor = ThemeHelpers.NavBarBackgroundColor; + navBar.BackgroundColor = ThemeHelpers.NavBarBackgroundColor; + navBar.TintColor = ThemeHelpers.NavBarTextColor; + navBar.TitleTextAttributes = new UIStringAttributes + { + ForegroundColor = ThemeHelpers.NavBarTextColor + }; } } } diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index fbce117ca..fb8d5e0f0 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -184,7 +184,7 @@ namespace Bit.iOS.Core.Controllers } } - public override async void ViewDidAppear(bool animated) + public override void ViewDidAppear(bool animated) { base.ViewDidAppear(animated); diff --git a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs index 4cde4b941..a732edaaf 100644 --- a/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs +++ b/src/iOS.Core/Utilities/AccountSwitchingOverlayHelper.cs @@ -12,10 +12,10 @@ namespace Bit.iOS.Core.Utilities public class AccountSwitchingOverlayHelper { const string DEFAULT_SYSTEM_AVATAR_IMAGE = "person.2"; - - IStateService _stateService; - IMessagingService _messagingService; - ILogger _logger; + + readonly IStateService _stateService; + readonly IMessagingService _messagingService; + readonly ILogger _logger; public AccountSwitchingOverlayHelper() { @@ -32,10 +32,12 @@ namespace Bit.iOS.Core.Utilities { throw new NullReferenceException(nameof(_stateService)); } - + var avatarImageSource = new AvatarImageSource(await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); - var avatarUIImage = await avatarImageSource.GetNativeImageAsync(); - return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync()) + { + return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); + } } catch (Exception ex) { diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 21321803e..a824d7235 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -2,6 +2,7 @@ using System.IO; using System.Threading.Tasks; using Bit.App.Abstractions; +using Bit.App.Controls; using Bit.App.Models; using Bit.App.Pages; using Bit.App.Resources; @@ -15,6 +16,7 @@ using Bit.iOS.Core.Services; using CoreNFC; using Foundation; using UIKit; +using Xamarin.Forms; namespace Bit.iOS.Core.Utilities { @@ -26,6 +28,42 @@ namespace Bit.iOS.Core.Utilities public static string AppGroupId = "group.com.8bit.bitwarden"; public static string AccessGroup = "LTZ2PFU5D6.com.8bit.bitwarden"; + public static void InitApp(T rootController, + string clearCipherCacheKey, + NFCNdefReaderSession nfcSession, + out NFCReaderDelegate nfcDelegate, + out IAccountsManager accountsManager) + where T : UIViewController, IAccountsManagerHost + { + Forms.Init(); + + if (ServiceContainer.RegisteredServices.Count > 0) + { + ServiceContainer.Reset(); + } + RegisterLocalServices(); + var deviceActionService = ServiceContainer.Resolve("deviceActionService"); + var messagingService = ServiceContainer.Resolve("messagingService"); + ServiceContainer.Init(deviceActionService.DeviceUserAgent, + clearCipherCacheKey, + Bit.Core.Constants.iOSAllClearCipherCacheKeys); + InitLogger(); + Bootstrap(); + + var appOptions = new AppOptions { IosExtension = true }; + var app = new App.App(appOptions); + ThemeManager.SetTheme(app.Resources); + + AppearanceAdjustments(); + + nfcDelegate = new Core.NFCReaderDelegate((success, message) => + messagingService.Send("gotYubiKeyOTP", message)); + SubscribeBroadcastReceiver(rootController, nfcSession, nfcDelegate); + + accountsManager = ServiceContainer.Resolve("accountsManager"); + accountsManager.Init(() => appOptions, rootController); + } + public static void InitLogger() { ServiceContainer.Resolve("logger").InitAsync(); @@ -89,6 +127,7 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Register("cryptoFunctionService", cryptoFunctionService); ServiceContainer.Register("cryptoService", cryptoService); ServiceContainer.Register("passwordRepromptService", passwordRepromptService); + ServiceContainer.Register("avatarImageSourcePool", new AvatarImageSourcePool()); } public static void Bootstrap(Func postBootstrapFunc = null) @@ -181,7 +220,8 @@ namespace Bit.iOS.Core.Utilities ServiceContainer.Resolve("secureStorageService"), ServiceContainer.Resolve("stateService"), ServiceContainer.Resolve("platformUtilsService"), - ServiceContainer.Resolve("authService")); + ServiceContainer.Resolve("authService"), + ServiceContainer.Resolve("logger")); ServiceContainer.Register("accountsManager", accountsManager); if (postBootstrapFunc != null) diff --git a/src/iOS.ShareExtension/ExtensionNavigationController.cs b/src/iOS.ShareExtension/ExtensionNavigationController.cs new file mode 100644 index 000000000..326ef2df0 --- /dev/null +++ b/src/iOS.ShareExtension/ExtensionNavigationController.cs @@ -0,0 +1,27 @@ +// This file has been autogenerated from a class added in the UI designer. + +using System; +using UIKit; + +namespace Bit.iOS.ShareExtension +{ + public partial class ExtensionNavigationController : UINavigationController + { + public ExtensionNavigationController (IntPtr handle) : base (handle) + { + } + + public override UIViewController PopViewController(bool animated) + { + TopViewController?.Dispose(); + return base.PopViewController(animated); + + } + + public override void DismissModalViewController(bool animated) + { + ModalViewController?.Dispose(); + base.DismissModalViewController(animated); + } + } +} diff --git a/src/iOS.ShareExtension/ExtensionNavigationController.designer.cs b/src/iOS.ShareExtension/ExtensionNavigationController.designer.cs new file mode 100644 index 000000000..aff73131b --- /dev/null +++ b/src/iOS.ShareExtension/ExtensionNavigationController.designer.cs @@ -0,0 +1,20 @@ +// WARNING +// +// This file has been generated automatically by Visual Studio to store outlets and +// actions made in the UI designer. If it is removed, they will be lost. +// Manual changes to this file may not be handled correctly. +// +using Foundation; +using System.CodeDom.Compiler; + +namespace Bit.iOS.ShareExtension +{ + [Register ("ExtensionNavigationController")] + partial class ExtensionNavigationController + { + + void ReleaseDesignerOutlets () + { + } + } +} diff --git a/src/iOS.ShareExtension/LoadingViewController.cs b/src/iOS.ShareExtension/LoadingViewController.cs index 6411ac175..8463c6795 100644 --- a/src/iOS.ShareExtension/LoadingViewController.cs +++ b/src/iOS.ShareExtension/LoadingViewController.cs @@ -7,11 +7,11 @@ using Bit.App.Abstractions; using Bit.App.Models; using Bit.App.Pages; using Bit.App.Utilities; +using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Utilities; -using Bit.iOS.Core; using Bit.iOS.Core.Controllers; using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Views; @@ -24,16 +24,33 @@ using Xamarin.Forms; namespace Bit.iOS.ShareExtension { - public partial class LoadingViewController : ExtendedUIViewController + public partial class LoadingViewController : ExtendedUIViewController, IAccountsManagerHost { + const string STORYBOARD_NAME = "MainInterface"; + private Context _context = new Context(); private NFCNdefReaderSession _nfcSession = null; private Core.NFCReaderDelegate _nfcDelegate = null; + private IAccountsManager _accountsManager; readonly LazyResolve _stateService = new LazyResolve("stateService"); readonly LazyResolve _vaultTimeoutService = new LazyResolve("vaultTimeoutService"); - readonly LazyResolve _deviceActionService = new LazyResolve("deviceActionService"); - readonly LazyResolve _eventService = new LazyResolve("eventService"); + + Lazy _storyboard = new Lazy(() => UIStoryboard.FromName(STORYBOARD_NAME, null)); + + private App.App _app = null; + private UIViewController _currentModalController; + private bool _presentingOnNavigationPage; + + private ExtensionNavigationController ExtNavigationController + { + get + { + NavigationController.PresentationController.Delegate = + new CustomPresentationControllerDelegate(CompleteRequest); + return NavigationController as ExtensionNavigationController; + } + } public LoadingViewController(IntPtr handle) : base(handle) @@ -41,39 +58,38 @@ namespace Bit.iOS.ShareExtension public override void ViewDidLoad() { - InitApp(); + iOSCoreHelpers.InitApp(this, Bit.Core.Constants.iOSShareExtensionClearCiphersCacheKey, + _nfcSession, out _nfcDelegate, out _accountsManager); base.ViewDidLoad(); Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png"); View.BackgroundColor = ThemeHelpers.SplashBackgroundColor; _context.ExtensionContext = ExtensionContext; + _context.ProviderType = GetProviderTypeFromExtensionInputItems(); + } + /// + /// Gets the provider given the input items + /// + private string GetProviderTypeFromExtensionInputItems() + { foreach (var item in ExtensionContext.InputItems) { - var processed = false; foreach (var itemProvider in item.Attachments) { if (itemProvider.HasItemConformingTo(UTType.PlainText)) { - _context.ProviderType = UTType.PlainText; - - processed = true; - break; + return UTType.PlainText; } - else if (itemProvider.HasItemConformingTo(UTType.Data)) + + if (itemProvider.HasItemConformingTo(UTType.Data)) { - _context.ProviderType = UTType.Data; - - processed = true; - break; + return UTType.Data; } } - if (processed) - { - break; - } } + return null; } public override async void ViewDidAppear(bool animated) @@ -84,12 +100,12 @@ namespace Bit.iOS.ShareExtension { if (!await IsAuthed()) { - LaunchHomePage(); + await _accountsManager.NavigateOnAccountChangeAsync(false); return; } else if (await IsLocked()) { - PerformSegue("lockPasswordSegue", this); + NavigateToLockViewController(); } else { @@ -102,24 +118,52 @@ namespace Bit.iOS.ShareExtension } } - public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) + void NavigateToLockViewController() { - if (segue.DestinationViewController is UINavigationController navController - && - navController.TopViewController is LockPasswordViewController passwordViewController) + var viewController = _storyboard.Value.InstantiateViewController("lockVC") as LockPasswordViewController; + viewController.LoadingController = this; + viewController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; + + if (_presentingOnNavigationPage) { - passwordViewController.LoadingController = this; - segue.DestinationViewController.PresentationController.Delegate = - new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction); + _presentingOnNavigationPage = false; + DismissViewController(true, () => ExtNavigationController.PushViewController(viewController, true)); + } + else + { + ExtNavigationController.PushViewController(viewController, true); } } public void DismissLockAndContinue() { Debug.WriteLine("BW Log, Dismissing lock controller."); + + ClearBeforeNavigating(); + DismissViewController(false, () => ContinueOnAsync().FireAndForget()); } + private void DismissAndLaunch(Action pageToLaunch) + { + ClearBeforeNavigating(); + + DismissViewController(false, pageToLaunch); + } + + void ClearBeforeNavigating() + { + _currentModalController?.Dispose(); + _currentModalController = null; + + if (_storyboard.IsValueCreated) + { + _storyboard.Value.Dispose(); + _storyboard = null; + _storyboard = new Lazy(() => UIStoryboard.FromName(STORYBOARD_NAME, null)); + } + } + private async Task ContinueOnAsync() { Tuple createSend = null; @@ -140,20 +184,24 @@ namespace Bit.iOS.ShareExtension CreateSend = createSend, CopyInsteadOfShareAfterSaving = true }; - var sendAddEditPage = new SendAddEditPage(appOptions) + var sendPage = new SendAddOnlyPage(appOptions) { OnClose = () => CompleteRequest(), AfterSubmit = () => CompleteRequest() }; - var app = new App.App(appOptions); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(sendAddEditPage); + SetupAppAndApplyResources(sendPage); - var navigationPage = new NavigationPage(sendAddEditPage); - var sendAddEditController = navigationPage.CreateViewController(); - sendAddEditController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(sendAddEditController, true, null); + NavigateToPage(sendPage); + } + + private void NavigateToPage(ContentPage page) + { + var navigationPage = new NavigationPage(page); + _currentModalController = navigationPage.CreateViewController(); + _currentModalController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; + _presentingOnNavigationPage = true; + PresentViewController(_currentModalController, true, null); } private async Task<(string, byte[])> LoadDataBytesAsync() @@ -202,31 +250,6 @@ namespace Bit.iOS.ShareExtension }); } - private void InitApp() - { - // Init Xamarin Forms - Forms.Init(); - - if (ServiceContainer.RegisteredServices.Count > 0) - { - ServiceContainer.Reset(); - } - iOSCoreHelpers.RegisterLocalServices(); - var messagingService = ServiceContainer.Resolve("messagingService"); - ServiceContainer.Init(_deviceActionService.Value.DeviceUserAgent, - Bit.Core.Constants.iOSShareExtensionClearCiphersCacheKey, Bit.Core.Constants.iOSAllClearCipherCacheKeys); - iOSCoreHelpers.InitLogger(); - iOSCoreHelpers.Bootstrap(); - - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - - iOSCoreHelpers.AppearanceAdjustments(); - _nfcDelegate = new NFCReaderDelegate((success, message) => - messagingService.Send("gotYubiKeyOTP", message)); - iOSCoreHelpers.SubscribeBroadcastReceiver(this, _nfcSession, _nfcDelegate); - } - private Task IsLocked() { return _vaultTimeoutService.Value.IsLockedAsync(); @@ -244,7 +267,7 @@ namespace Bit.iOS.ShareExtension if (await IsAuthed()) { await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); - if (_deviceActionService.Value.SystemMajorVersion() >= 12) + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) { await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); } @@ -252,83 +275,75 @@ namespace Bit.iOS.ShareExtension }); } + private App.App SetupAppAndApplyResources(ContentPage page) + { + if (_app is null) + { + var app = new App.App(new AppOptions { IosExtension = true }); + ThemeManager.SetTheme(app.Resources); + } + ThemeManager.ApplyResourcesToPage(page); + return _app; + } + private void LaunchHomePage() { var homePage = new HomePage(); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(homePage); + SetupAppAndApplyResources(homePage); if (homePage.BindingContext is HomeViewModel vm) { - vm.StartLoginAction = () => DismissViewController(false, () => LaunchLoginFlow()); - vm.StartRegisterAction = () => DismissViewController(false, () => LaunchRegisterFlow()); - vm.StartSsoLoginAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); - vm.StartEnvironmentAction = () => DismissViewController(false, () => LaunchEnvironmentFlow()); + vm.StartLoginAction = () => DismissAndLaunch(() => LaunchLoginFlow()); + vm.StartRegisterAction = () => DismissAndLaunch(() => LaunchRegisterFlow()); + vm.StartSsoLoginAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow()); + vm.StartEnvironmentAction = () => DismissAndLaunch(() => LaunchEnvironmentFlow()); vm.CloseAction = () => CompleteRequest(); } - var navigationPage = new NavigationPage(homePage); - var loginController = navigationPage.CreateViewController(); - loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(loginController, true, null); - + NavigateToPage(homePage); LogoutIfAuthed(); } private void LaunchEnvironmentFlow() { var environmentPage = new EnvironmentPage(); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); + SetupAppAndApplyResources(environmentPage); ThemeManager.ApplyResourcesToPage(environmentPage); if (environmentPage.BindingContext is EnvironmentPageViewModel vm) { - vm.SubmitSuccessAction = () => DismissViewController(false, () => LaunchHomePage()); - vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); + vm.SubmitSuccessAction = () => DismissAndLaunch(() => LaunchHomePage()); + vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage()); } - var navigationPage = new NavigationPage(environmentPage); - var loginController = navigationPage.CreateViewController(); - loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(loginController, true, null); + NavigateToPage(environmentPage); } private void LaunchRegisterFlow() { var registerPage = new RegisterPage(null); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(registerPage); + SetupAppAndApplyResources(registerPage); if (registerPage.BindingContext is RegisterPageViewModel vm) { - vm.RegistrationSuccess = () => DismissViewController(false, () => LaunchLoginFlow(vm.Email)); - vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); + vm.RegistrationSuccess = () => DismissAndLaunch(() => LaunchLoginFlow(vm.Email)); + vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage()); } - - var navigationPage = new NavigationPage(registerPage); - var loginController = navigationPage.CreateViewController(); - loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(loginController, true, null); + NavigateToPage(registerPage); } private void LaunchLoginFlow(string email = null) { var loginPage = new LoginPage(email); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(loginPage); + SetupAppAndApplyResources(loginPage); if (loginPage.BindingContext is LoginPageViewModel vm) { - vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(false)); - vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); - vm.LogInSuccessAction = () => DismissLockAndContinue(); + vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(false)); + vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow()); + vm.LogInSuccessAction = () => + { + DismissLockAndContinue(); + }; vm.CloseAction = () => CompleteRequest(); } - - var navigationPage = new NavigationPage(loginPage); - var loginController = navigationPage.CreateViewController(); - loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(loginController, true, null); + NavigateToPage(loginPage); LogoutIfAuthed(); } @@ -336,22 +351,16 @@ namespace Bit.iOS.ShareExtension private void LaunchLoginSsoFlow() { var loginPage = new LoginSsoPage(); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(loginPage); + SetupAppAndApplyResources(loginPage); if (loginPage.BindingContext is LoginSsoPageViewModel vm) { - vm.StartTwoFactorAction = () => DismissViewController(false, () => LaunchTwoFactorFlow(true)); - vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); - vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); + vm.StartTwoFactorAction = () => DismissAndLaunch(() => LaunchTwoFactorFlow(true)); + vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow()); + vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow()); vm.SsoAuthSuccessAction = () => DismissLockAndContinue(); - vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); + vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage()); } - - var navigationPage = new NavigationPage(loginPage); - var loginController = navigationPage.CreateViewController(); - loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(loginController, true, null); + NavigateToPage(loginPage); LogoutIfAuthed(); } @@ -359,65 +368,97 @@ namespace Bit.iOS.ShareExtension private void LaunchTwoFactorFlow(bool authingWithSso) { var twoFactorPage = new TwoFactorPage(); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(twoFactorPage); + SetupAppAndApplyResources(twoFactorPage); if (twoFactorPage.BindingContext is TwoFactorPageViewModel vm) { vm.TwoFactorAuthSuccessAction = () => DismissLockAndContinue(); - vm.StartSetPasswordAction = () => DismissViewController(false, () => LaunchSetPasswordFlow()); + vm.StartSetPasswordAction = () => DismissAndLaunch(() => LaunchSetPasswordFlow()); if (authingWithSso) { - vm.CloseAction = () => DismissViewController(false, () => LaunchLoginSsoFlow()); + vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginSsoFlow()); } else { - vm.CloseAction = () => DismissViewController(false, () => LaunchLoginFlow()); + vm.CloseAction = () => DismissAndLaunch(() => LaunchLoginFlow()); } - vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); + vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow()); } - - var navigationPage = new NavigationPage(twoFactorPage); - var twoFactorController = navigationPage.CreateViewController(); - twoFactorController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(twoFactorController, true, null); + NavigateToPage(twoFactorPage); } private void LaunchSetPasswordFlow() { var setPasswordPage = new SetPasswordPage(); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(setPasswordPage); + SetupAppAndApplyResources(setPasswordPage); if (setPasswordPage.BindingContext is SetPasswordPageViewModel vm) { - vm.UpdateTempPasswordAction = () => DismissViewController(false, () => LaunchUpdateTempPasswordFlow()); + vm.UpdateTempPasswordAction = () => DismissAndLaunch(() => LaunchUpdateTempPasswordFlow()); vm.SetPasswordSuccessAction = () => DismissLockAndContinue(); - vm.CloseAction = () => DismissViewController(false, () => LaunchHomePage()); + vm.CloseAction = () => DismissAndLaunch(() => LaunchHomePage()); } - - var navigationPage = new NavigationPage(setPasswordPage); - var setPasswordController = navigationPage.CreateViewController(); - setPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(setPasswordController, true, null); + NavigateToPage(setPasswordPage); } private void LaunchUpdateTempPasswordFlow() { var updateTempPasswordPage = new UpdateTempPasswordPage(); - var app = new App.App(new AppOptions { IosExtension = true }); - ThemeManager.SetTheme(app.Resources); - ThemeManager.ApplyResourcesToPage(updateTempPasswordPage); + SetupAppAndApplyResources(updateTempPasswordPage); if (updateTempPasswordPage.BindingContext is UpdateTempPasswordPageViewModel vm) { - vm.UpdateTempPasswordSuccessAction = () => DismissViewController(false, () => LaunchHomePage()); - vm.LogOutAction = () => DismissViewController(false, () => LaunchHomePage()); + vm.UpdateTempPasswordSuccessAction = () => DismissAndLaunch(() => LaunchHomePage()); + vm.LogOutAction = () => DismissAndLaunch(() => LaunchHomePage()); + } + NavigateToPage(updateTempPasswordPage); + } + + public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null) + { + if (ExtNavigationController?.ViewControllers?.Any() ?? false) + { + ExtNavigationController.PopViewController(false); + } + else if (ExtNavigationController?.ModalViewController != null) + { + ExtNavigationController.DismissModalViewController(false); } - var navigationPage = new NavigationPage(updateTempPasswordPage); - var updateTempPasswordController = navigationPage.CreateViewController(); - updateTempPasswordController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; - PresentViewController(updateTempPasswordController, true, null); + switch (navTarget) + { + case NavigationTarget.HomeLogin: + ExecuteLaunch(LaunchHomePage); + break; + case NavigationTarget.Login: + if (navParams is LoginNavigationParams loginParams) + { + ExecuteLaunch(() => LaunchLoginFlow(loginParams.Email)); + } + else + { + ExecuteLaunch(() => LaunchLoginFlow()); + } + break; + case NavigationTarget.Lock: + NavigateToLockViewController(); + break; + case NavigationTarget.Home: + DismissLockAndContinue(); + break; + } } + + private void ExecuteLaunch(Action launchAction) + { + if (_presentingOnNavigationPage) + { + DismissAndLaunch(launchAction); + } + else + { + launchAction(); + } + } + + public Task SetPreviousPageInfoAsync() => Task.CompletedTask; + public Task UpdateThemeAsync() => Task.CompletedTask; } } diff --git a/src/iOS.ShareExtension/LockPasswordViewController.cs b/src/iOS.ShareExtension/LockPasswordViewController.cs index 7a1b599f7..d8508981c 100644 --- a/src/iOS.ShareExtension/LockPasswordViewController.cs +++ b/src/iOS.ShareExtension/LockPasswordViewController.cs @@ -1,11 +1,22 @@ +using Bit.App.Controls; +using Bit.Core.Utilities; using Bit.iOS.Core.Utilities; using System; using UIKit; namespace Bit.iOS.ShareExtension { - public partial class LockPasswordViewController : Core.Controllers.LockPasswordViewController + public partial class LockPasswordViewController : Core.Controllers.BaseLockPasswordViewController { + AccountSwitchingOverlayView _accountSwitchingOverlayView; + AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; + + public LockPasswordViewController() + { + BiometricIntegrityKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegrityKey; + DismissModalAction = Cancel; + } + public LockPasswordViewController(IntPtr handle) : base(handle) { @@ -17,24 +28,80 @@ namespace Bit.iOS.ShareExtension public override UINavigationItem BaseNavItem => _navItem; public override UIBarButtonItem BaseCancelButton => _cancelButton; public override UIBarButtonItem BaseSubmitButton => _submitButton; - public override Action Success => () => LoadingController.DismissLockAndContinue(); - public override Action Cancel => () => LoadingController.CompleteRequest(); + public override Action Success => () => + { + LoadingController?.Navigate(Bit.Core.Enums.NavigationTarget.Home); + LoadingController = null; + }; + public override Action Cancel => () => + { + LoadingController?.CompleteRequest(); + LoadingController = null; + }; - public override void ViewDidLoad() + public override UITableView TableView => _mainTableView; + + public override async void ViewDidLoad() { base.ViewDidLoad(); + _cancelButton.TintColor = ThemeHelpers.NavBarTextColor; _submitButton.TintColor = ThemeHelpers.NavBarTextColor; + + _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); + _accountSwitchingButton.Image = await _accountSwitchingOverlayHelper.CreateAvatarImageAsync(); + + _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(_overlayView); + } + + protected override void UpdateNavigationBarTheme() + { + UpdateNavigationBarTheme(_navBar); + } + + partial void AccountSwitchingButton_Activated(UIBarButtonItem sender) + { + _accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, _overlayView); } partial void SubmitButton_Activated(UIBarButtonItem sender) { - var task = CheckPasswordAsync(); + CheckPasswordAsync().FireAndForget(); } partial void CancelButton_Activated(UIBarButtonItem sender) { Cancel(); } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (TableView != null) + { + TableView.Source?.Dispose(); + } + if (_accountSwitchingButton?.Image != null) + { + var img = _accountSwitchingButton.Image; + _accountSwitchingButton.Image = null; + img.Dispose(); + } + if (_accountSwitchingOverlayView != null && _overlayView?.Subviews != null) + { + foreach (var subView in _overlayView.Subviews) + { + subView.RemoveFromSuperview(); + subView.Dispose(); + } + _accountSwitchingOverlayView = null; + _overlayView.RemoveFromSuperview(); + } + _accountSwitchingOverlayHelper = null; + } + + base.Dispose(disposing); + } } } diff --git a/src/iOS.ShareExtension/LockPasswordViewController.designer.cs b/src/iOS.ShareExtension/LockPasswordViewController.designer.cs index b92a198b0..6be61f9dc 100644 --- a/src/iOS.ShareExtension/LockPasswordViewController.designer.cs +++ b/src/iOS.ShareExtension/LockPasswordViewController.designer.cs @@ -12,18 +12,30 @@ namespace Bit.iOS.ShareExtension [Register ("LockPasswordViewController")] partial class LockPasswordViewController { + [Outlet] + UIKit.UIBarButtonItem _accountSwitchingButton { get; set; } + [Outlet] UIKit.UIBarButtonItem _cancelButton { get; set; } [Outlet] UIKit.UITableView _mainTableView { get; set; } + [Outlet] + UIKit.UINavigationBar _navBar { get; set; } + [Outlet] UIKit.UINavigationItem _navItem { get; set; } + [Outlet] + UIKit.UIView _overlayView { get; set; } + [Outlet] UIKit.UIBarButtonItem _submitButton { get; set; } + [Action ("AccountSwitchingButton_Activated:")] + partial void AccountSwitchingButton_Activated (UIKit.UIBarButtonItem sender); + [Action ("CancelButton_Activated:")] partial void CancelButton_Activated (UIKit.UIBarButtonItem sender); @@ -32,6 +44,11 @@ namespace Bit.iOS.ShareExtension void ReleaseDesignerOutlets () { + if (_accountSwitchingButton != null) { + _accountSwitchingButton.Dispose (); + _accountSwitchingButton = null; + } + if (_cancelButton != null) { _cancelButton.Dispose (); _cancelButton = null; @@ -47,10 +64,20 @@ namespace Bit.iOS.ShareExtension _navItem = null; } + if (_overlayView != null) { + _overlayView.Dispose (); + _overlayView = null; + } + if (_submitButton != null) { _submitButton.Dispose (); _submitButton = null; } + + if (_navBar != null) { + _navBar.Dispose (); + _navBar = null; + } } } } diff --git a/src/iOS.ShareExtension/MainInterface.storyboard b/src/iOS.ShareExtension/MainInterface.storyboard index 98a8e1346..1bde113e1 100644 --- a/src/iOS.ShareExtension/MainInterface.storyboard +++ b/src/iOS.ShareExtension/MainInterface.storyboard @@ -1,9 +1,11 @@ - + - + + + @@ -11,10 +13,6 @@ - - - - @@ -23,25 +21,25 @@ + - + - - + - + - - + + - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + - - + + - + + + + + diff --git a/src/iOS.ShareExtension/iOS.ShareExtension.csproj b/src/iOS.ShareExtension/iOS.ShareExtension.csproj index 7d8bebc6d..5109c8886 100644 --- a/src/iOS.ShareExtension/iOS.ShareExtension.csproj +++ b/src/iOS.ShareExtension/iOS.ShareExtension.csproj @@ -26,7 +26,6 @@ None x86_64 NSUrlSessionHandler - false Entitlements.plist BitwardeniOSShareExtension @@ -193,6 +192,10 @@ LockPasswordViewController.cs + + + ExtensionNavigationController.cs + From f2ba86a62b848a2dde36e3733b783e9a58a4b1fd Mon Sep 17 00:00:00 2001 From: vincentvidal Date: Thu, 14 Jul 2022 22:24:02 +0200 Subject: [PATCH 16/23] =?UTF-8?q?Add=20support=20for=20iod=C3=A9=20Browser?= =?UTF-8?q?=20(#1886)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Android/Accessibility/AccessibilityHelpers.cs | 1 + src/Android/Autofill/AutofillHelpers.cs | 1 + src/Android/Resources/xml/autofillservice.xml | 3 +++ 3 files changed, 5 insertions(+) diff --git a/src/Android/Accessibility/AccessibilityHelpers.cs b/src/Android/Accessibility/AccessibilityHelpers.cs index 82bb3b4a5..464c7abad 100644 --- a/src/Android/Accessibility/AccessibilityHelpers.cs +++ b/src/Android/Accessibility/AccessibilityHelpers.cs @@ -54,6 +54,7 @@ namespace Bit.Droid.Accessibility new Browser("com.google.android.apps.chrome", "url_bar"), new Browser("com.google.android.apps.chrome_dev", "url_bar"), // Rem. for "com.google.android.captiveportallogin": URL displayed in ActionBar subtitle without viewId. + new Browser("com.iode.firefox", "mozac_browser_toolbar_url_view"), new Browser("com.jamal2367.styx", "search"), new Browser("com.kiwibrowser.browser", "url_bar"), new Browser("com.kiwibrowser.browser.dev", "url_bar"), diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index 4549e56b8..d9438ac64 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -73,6 +73,7 @@ namespace Bit.Droid.Autofill "com.google.android.apps.chrome", "com.google.android.apps.chrome_dev", "com.google.android.captiveportallogin", + "com.iode.firefox", "com.jamal2367.styx", "com.kiwibrowser.browser", "com.kiwibrowser.browser.dev", diff --git a/src/Android/Resources/xml/autofillservice.xml b/src/Android/Resources/xml/autofillservice.xml index c65812815..0cec265ba 100644 --- a/src/Android/Resources/xml/autofillservice.xml +++ b/src/Android/Resources/xml/autofillservice.xml @@ -77,6 +77,9 @@ + From 70cf7431f79a4178c1db94810817a7dddc28f157 Mon Sep 17 00:00:00 2001 From: Donkeykong307 Date: Thu, 14 Jul 2022 21:27:53 +0100 Subject: [PATCH 17/23] Opera GX Autofill Support (#1855) Added Opera GX Support for autofill --- src/Android/Accessibility/AccessibilityHelpers.cs | 1 + src/Android/Autofill/AutofillHelpers.cs | 1 + src/Android/Resources/xml/autofillservice.xml | 3 +++ 3 files changed, 5 insertions(+) diff --git a/src/Android/Accessibility/AccessibilityHelpers.cs b/src/Android/Accessibility/AccessibilityHelpers.cs index 464c7abad..81d7fa65f 100644 --- a/src/Android/Accessibility/AccessibilityHelpers.cs +++ b/src/Android/Accessibility/AccessibilityHelpers.cs @@ -68,6 +68,7 @@ namespace Bit.Droid.Accessibility new Browser("com.naver.whale", "url_bar"), new Browser("com.opera.browser", "url_field"), new Browser("com.opera.browser.beta", "url_field"), + new Browser("com.opera.gx", "addressbarEdit"), new Browser("com.opera.mini.native", "url_field"), new Browser("com.opera.mini.native.beta", "url_field"), new Browser("com.opera.touch", "addressbarEdit"), diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index d9438ac64..8ec3416bd 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -87,6 +87,7 @@ namespace Bit.Droid.Autofill "com.naver.whale", "com.opera.browser", "com.opera.browser.beta", + "com.opera.gx", "com.opera.mini.native", "com.opera.mini.native.beta", "com.opera.touch", diff --git a/src/Android/Resources/xml/autofillservice.xml b/src/Android/Resources/xml/autofillservice.xml index 0cec265ba..f47e35f8c 100644 --- a/src/Android/Resources/xml/autofillservice.xml +++ b/src/Android/Resources/xml/autofillservice.xml @@ -119,6 +119,9 @@ + From 8f3a4b98a585ad8f2834f2d932a4ef4754e28a36 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 14 Jul 2022 17:33:30 -0300 Subject: [PATCH 18/23] EC-323 sanitize data on get first letters for avatar image creation (#1990) --- src/App/Controls/AvatarImageSource.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/App/Controls/AvatarImageSource.cs b/src/App/Controls/AvatarImageSource.cs index 4b5902618..38f3df31c 100644 --- a/src/App/Controls/AvatarImageSource.cs +++ b/src/App/Controls/AvatarImageSource.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using SkiaSharp; @@ -131,21 +132,23 @@ namespace Bit.App.Controls private string GetFirstLetters(string data, int charCount) { - var parts = data.Split(); + var sanitizedData = data.Trim(); + var parts = sanitizedData.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length > 1 && charCount <= 2) { - var text = ""; - for (int i = 0; i < charCount; i++) + var text = string.Empty; + for (var i = 0; i < charCount; i++) { - text += parts[i].Substring(0, 1); + text += parts[i][0]; } return text; } - if (data.Length > 2) + if (sanitizedData.Length > 2) { - return data.Substring(0, 2); + return sanitizedData.Substring(0, 2); } - return data; + return sanitizedData; } private Color StringToColor(string str) From 1f2fb3f796617e542c540aca55f65d5abb34c529 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 14 Jul 2022 18:54:45 -0300 Subject: [PATCH 19/23] [EC-324] Added more logging for information on list crash (#1993) * EC-324 Added more logging for trying to get more information on list out of range crash on AppCenter * EC-324 Fix include on iOS.Core.csproj on iOS CollectionView files --- .../CollectionView/CollectionException.cs | 16 +++++++ .../ExtendedGroupableItemsViewController.cs | 19 +++++++- .../ExtendedGroupableItemsViewDelegator.cs | 43 +++++++++++++++++++ src/iOS.Core/iOS.Core.csproj | 2 + 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/iOS.Core/Renderers/CollectionView/CollectionException.cs create mode 100644 src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewDelegator.cs diff --git a/src/iOS.Core/Renderers/CollectionView/CollectionException.cs b/src/iOS.Core/Renderers/CollectionView/CollectionException.cs new file mode 100644 index 000000000..35204bf95 --- /dev/null +++ b/src/iOS.Core/Renderers/CollectionView/CollectionException.cs @@ -0,0 +1,16 @@ +using System; +namespace Bit.iOS.Core.Renderers.CollectionView +{ + public class CollectionException : Exception + { + public CollectionException(string message) + : base(message) + { + } + + public CollectionException(string message, Exception innerEx) + : base(message, innerEx) + { + } + } +} diff --git a/src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewController.cs b/src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewController.cs index 94c15e114..519fcbbb6 100644 --- a/src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewController.cs +++ b/src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewController.cs @@ -1,6 +1,8 @@ using System; using Bit.App.Controls; +using Bit.Core.Services; using Foundation; +using UIKit; using Xamarin.Forms.Platform.iOS; namespace Bit.iOS.Core.Renderers.CollectionView @@ -13,6 +15,11 @@ namespace Bit.iOS.Core.Renderers.CollectionView { } + protected override UICollectionViewDelegateFlowLayout CreateDelegator() + { + return new ExtendedGroupableItemsViewDelegator>(ItemsViewLayout, this); + } + protected override void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath indexPath) { try @@ -21,7 +28,17 @@ namespace Bit.iOS.Core.Renderers.CollectionView } catch (Exception ex) when (ItemsView?.ExtraDataForLogging != null) { - throw new Exception("Error in ExtendedCollectionView, extra data: " + ItemsView.ExtraDataForLogging, ex); + var colEx = new CollectionException("Error in ExtendedCollectionView -> ExtendedGroupableItemsViewController, extra data: " + ItemsView.ExtraDataForLogging, ex); + try + { + LoggerHelper.LogEvenIfCantBeResolved(colEx); + } + catch + { + // Do nothing in here, this is temporary to get more info about the crash, if the logger fails, we want to get the info + // by crashing with the original exception and not the logger one + } + throw colEx; } } } diff --git a/src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewDelegator.cs b/src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewDelegator.cs new file mode 100644 index 000000000..d6ccb30fe --- /dev/null +++ b/src/iOS.Core/Renderers/CollectionView/ExtendedGroupableItemsViewDelegator.cs @@ -0,0 +1,43 @@ +using System; +using Bit.App.Controls; +using Bit.Core.Services; +using CoreGraphics; +using Foundation; +using UIKit; +using Xamarin.Forms.Platform.iOS; + +namespace Bit.iOS.Core.Renderers.CollectionView +{ + public class ExtendedGroupableItemsViewDelegator : GroupableItemsViewDelegator + where TItemsView : ExtendedCollectionView + where TViewController : GroupableItemsViewController + { + public ExtendedGroupableItemsViewDelegator(ItemsViewLayout itemsViewLayout, TViewController itemsViewController) + : base(itemsViewLayout, itemsViewController) + { + } + + public override CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath) + { + // Added this to get extra information on a crash when getting the size for an item. + try + { + return base.GetSizeForItem(collectionView, layout, indexPath); + } + catch (Exception ex) when (ViewController?.ItemsView?.ExtraDataForLogging != null) + { + var colEx = new CollectionException("Error in ExtendedCollectionView -> ExtendedGroupableItemsViewDelegator, extra data: " + ViewController.ItemsView.ExtraDataForLogging, ex); + try + { + LoggerHelper.LogEvenIfCantBeResolved(colEx); + } + catch + { + // Do nothing in here, this is temporary to get more info about the crash, if the logger fails, we want to get the info + // by crashing with the original exception and not the logger one + } + throw colEx; + } + } + } +} diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj index 4344ddb6d..9cb52e91a 100644 --- a/src/iOS.Core/iOS.Core.csproj +++ b/src/iOS.Core/iOS.Core.csproj @@ -201,6 +201,8 @@ + + From 2d2a883b963adb7506bdf5c1c548ad5ff46b2279 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 14 Jul 2022 19:04:13 -0300 Subject: [PATCH 20/23] EC-306 Fix crash happening on vietnamise when trying to go to Password Autofill on iOS given that the string was the same as Autofill Services and the comparison was misleading. Also refactored so that the action is on each item instead of having to compare to act (#1989) --- .../SettingsPage/SettingsPage.xaml.cs | 122 +--------------- .../SettingsPage/SettingsPageListItem.cs | 3 + .../SettingsPage/SettingsPageViewModel.cs | 136 ++++++++++++++---- 3 files changed, 118 insertions(+), 143 deletions(-) diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs index ac0cfdf04..47a8a22c4 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPage.xaml.cs @@ -2,18 +2,13 @@ using System.ComponentModel; using System.Linq; using System.Threading.Tasks; -using Bit.App.Abstractions; using Bit.App.Controls; -using Bit.App.Pages.Accounts; -using Bit.App.Resources; -using Bit.Core.Utilities; using Xamarin.Forms; namespace Bit.App.Pages { public partial class SettingsPage : BaseContentPage { - private readonly IDeviceActionService _deviceActionService; private readonly TabsPage _tabsPage; private SettingsPageViewModel _vm; @@ -21,7 +16,6 @@ namespace Bit.App.Pages { _tabsPage = tabsPage; InitializeComponent(); - _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _vm = BindingContext as SettingsPageViewModel; _vm.Page = this; } @@ -67,122 +61,12 @@ namespace Bit.App.Pages } } - private async void RowSelected(object sender, SelectionChangedEventArgs e) + private void RowSelected(object sender, SelectionChangedEventArgs e) { ((ExtendedCollectionView)sender).SelectedItem = null; - if (!DoOnce()) + if (e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item) { - return; - } - if (!(e.CurrentSelection?.FirstOrDefault() is SettingsPageListItem item)) - { - return; - } - - if (item.Name == AppResources.Sync) - { - await Navigation.PushModalAsync(new NavigationPage(new SyncPage())); - } - else if (item.Name == AppResources.AutofillServices) - { - await Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(this))); - } - else if (item.Name == AppResources.PasswordAutofill) - { - await Navigation.PushModalAsync(new NavigationPage(new AutofillPage())); - } - else if (item.Name == AppResources.AppExtension) - { - await Navigation.PushModalAsync(new NavigationPage(new ExtensionPage())); - } - else if (item.Name == AppResources.Options) - { - await Navigation.PushModalAsync(new NavigationPage(new OptionsPage())); - } - else if (item.Name == AppResources.Folders) - { - await Navigation.PushModalAsync(new NavigationPage(new FoldersPage())); - } - else if (item.Name == AppResources.About) - { - await _vm.AboutAsync(); - } - else if (item.Name == AppResources.HelpAndFeedback) - { - _vm.Help(); - } - else if (item.Name == AppResources.FingerprintPhrase) - { - await _vm.FingerprintAsync(); - } - else if (item.Name == AppResources.RateTheApp) - { - _vm.Rate(); - } - else if (item.Name == AppResources.ImportItems) - { - _vm.Import(); - } - else if (item.Name == AppResources.ExportVault) - { - await Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage())); - } - else if (item.Name == AppResources.LearnOrg) - { - await _vm.ShareAsync(); - } - else if (item.Name == AppResources.WebVault) - { - _vm.WebVault(); - } - else if (item.Name == AppResources.ChangeMasterPassword) - { - await _vm.ChangePasswordAsync(); - } - else if (item.Name == AppResources.TwoStepLogin) - { - await _vm.TwoStepAsync(); - } - else if (item.Name == AppResources.LogOut) - { - await _vm.LogOutAsync(); - } - else if (item.Name == AppResources.DeleteAccount) - { - await Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage())); - } - else if (item.Name == AppResources.LockNow) - { - await _vm.LockAsync(); - } - else if (item.Name == AppResources.VaultTimeout) - { - await _vm.VaultTimeoutAsync(); - } - else if (item.Name == AppResources.VaultTimeoutAction) - { - await _vm.VaultTimeoutActionAsync(); - } - else if (item.Name == AppResources.UnlockWithPIN) - { - await _vm.UpdatePinAsync(); - } - else if (item.Name == AppResources.SubmitCrashLogs) - { - await _vm.LoggerReportingAsync(); - } - else - { - var biometricName = AppResources.Biometrics; - if (Device.RuntimePlatform == Device.iOS) - { - var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); - biometricName = supportsFace ? AppResources.FaceID : AppResources.TouchID; - } - if (item.Name == string.Format(AppResources.UnlockWith, biometricName)) - { - await _vm.UpdateBiometricAsync(); - } + _vm?.ExecuteSettingItemCommand.Execute(item); } } } diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs index c1e8878f4..a4d3e926d 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageListItem.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Bit.App.Resources; using Bit.App.Utilities; using Xamarin.Forms; @@ -12,6 +13,8 @@ namespace Bit.App.Pages public string SubLabel { get; set; } public TimeSpan? Time { get; set; } public bool UseFrame { get; set; } + public Func ExecuteAsync { get; set; } + public bool SubLabelTextEnabled => SubLabel == AppResources.Enabled; public string LineBreakMode => SubLabel == null ? "TailTruncation" : ""; public bool ShowSubLabel => SubLabel.Length != 0; diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs index 831be4979..59d76f762 100644 --- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs +++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bit.App.Abstractions; +using Bit.App.Pages.Accounts; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; @@ -84,10 +85,14 @@ namespace Bit.App.Pages GroupedItems = new ObservableRangeCollection(); PageTitle = AppResources.Settings; + + ExecuteSettingItemCommand = new AsyncCommand(item => item.ExecuteAsync(), onException: _loggerService.Exception, allowsMultipleExecutions: false); } public ObservableRangeCollection GroupedItems { get; set; } + public IAsyncCommand ExecuteSettingItemCommand { get; } + public async Task InitAsync() { _supportsBiometric = await _platformUtilsService.SupportsBiometricAsync(); @@ -434,6 +439,8 @@ namespace Bit.App.Pages public void BuildList() { + //TODO: Refactor this once navigation is abstracted so that it doesn't depend on Page, e.g. Page.Navigation.PushModalAsync... + var doUpper = Device.RuntimePlatform != Device.Android; var autofillItems = new List(); if (Device.RuntimePlatform == Device.Android) @@ -441,38 +448,69 @@ namespace Bit.App.Pages autofillItems.Add(new SettingsPageListItem { Name = AppResources.AutofillServices, - SubLabel = _deviceActionService.AutofillServicesEnabled() ? - AppResources.Enabled : AppResources.Disabled + SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.Enabled : AppResources.Disabled, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage))) }); } else { if (_deviceActionService.SystemMajorVersion() >= 12) { - autofillItems.Add(new SettingsPageListItem { Name = AppResources.PasswordAutofill }); + autofillItems.Add(new SettingsPageListItem + { + Name = AppResources.PasswordAutofill, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillPage())) + }); } - autofillItems.Add(new SettingsPageListItem { Name = AppResources.AppExtension }); + autofillItems.Add(new SettingsPageListItem + { + Name = AppResources.AppExtension, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExtensionPage())) + }); } var manageItems = new List { - new SettingsPageListItem { Name = AppResources.Folders }, - new SettingsPageListItem { Name = AppResources.Sync, SubLabel = _lastSyncDate } + new SettingsPageListItem + { + Name = AppResources.Folders, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new FoldersPage())) + }, + new SettingsPageListItem + { + Name = AppResources.Sync, + SubLabel = _lastSyncDate, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new SyncPage())) + } }; var securityItems = new List { - new SettingsPageListItem { Name = AppResources.VaultTimeout, SubLabel = _vaultTimeoutDisplayValue }, + new SettingsPageListItem + { + Name = AppResources.VaultTimeout, + SubLabel = _vaultTimeoutDisplayValue, + ExecuteAsync = () => VaultTimeoutAsync() }, new SettingsPageListItem { Name = AppResources.VaultTimeoutAction, - SubLabel = _vaultTimeoutActionDisplayValue + SubLabel = _vaultTimeoutActionDisplayValue, + ExecuteAsync = () => VaultTimeoutActionAsync() }, new SettingsPageListItem { Name = AppResources.UnlockWithPIN, - SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled + SubLabel = _pin ? AppResources.Enabled : AppResources.Disabled, + ExecuteAsync = () => UpdatePinAsync() }, - new SettingsPageListItem { Name = AppResources.LockNow }, - new SettingsPageListItem { Name = AppResources.TwoStepLogin } + new SettingsPageListItem + { + Name = AppResources.LockNow, + ExecuteAsync = () => LockAsync() + }, + new SettingsPageListItem + { + Name = AppResources.TwoStepLogin, + ExecuteAsync = () => TwoStepAsync() + } }; if (_supportsBiometric || _biometric) { @@ -485,7 +523,8 @@ namespace Bit.App.Pages var item = new SettingsPageListItem { Name = string.Format(AppResources.UnlockWith, biometricName), - SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled + SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled, + ExecuteAsync = () => UpdateBiometricAsync() }; securityItems.Insert(2, item); } @@ -510,38 +549,87 @@ namespace Bit.App.Pages } var accountItems = new List { - new SettingsPageListItem { Name = AppResources.FingerprintPhrase }, - new SettingsPageListItem { Name = AppResources.LogOut } + new SettingsPageListItem + { + Name = AppResources.FingerprintPhrase, + ExecuteAsync = () => FingerprintAsync() + }, + new SettingsPageListItem + { + Name = AppResources.LogOut, + ExecuteAsync = () => LogOutAsync() + } }; if (_showChangeMasterPassword) { - accountItems.Insert(0, new SettingsPageListItem { Name = AppResources.ChangeMasterPassword }); + accountItems.Insert(0, new SettingsPageListItem + { + Name = AppResources.ChangeMasterPassword, + ExecuteAsync = () => ChangePasswordAsync() + }); } var toolsItems = new List { - new SettingsPageListItem { Name = AppResources.ImportItems }, - new SettingsPageListItem { Name = AppResources.ExportVault } + new SettingsPageListItem + { + Name = AppResources.ImportItems, + ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Import()) + }, + new SettingsPageListItem + { + Name = AppResources.ExportVault, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new ExportVaultPage())) + } }; if (IncludeLinksWithSubscriptionInfo()) { - toolsItems.Add(new SettingsPageListItem { Name = AppResources.LearnOrg }); - toolsItems.Add(new SettingsPageListItem { Name = AppResources.WebVault }); + toolsItems.Add(new SettingsPageListItem + { + Name = AppResources.LearnOrg, + ExecuteAsync = () => ShareAsync() + }); + toolsItems.Add(new SettingsPageListItem + { + Name = AppResources.WebVault, + ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => WebVault()) + }); } var otherItems = new List { - new SettingsPageListItem { Name = AppResources.Options }, - new SettingsPageListItem { Name = AppResources.About }, - new SettingsPageListItem { Name = AppResources.HelpAndFeedback }, + new SettingsPageListItem + { + Name = AppResources.Options, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new OptionsPage())) + }, + new SettingsPageListItem + { + Name = AppResources.About, + ExecuteAsync = () => AboutAsync() + }, + new SettingsPageListItem + { + Name = AppResources.HelpAndFeedback, + ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Help()) + }, #if !FDROID new SettingsPageListItem { Name = AppResources.SubmitCrashLogs, SubLabel = _reportLoggingEnabled ? AppResources.Enabled : AppResources.Disabled, + ExecuteAsync = () => LoggerReportingAsync() }, #endif - new SettingsPageListItem { Name = AppResources.RateTheApp }, - new SettingsPageListItem { Name = AppResources.DeleteAccount } + new SettingsPageListItem + { + Name = AppResources.RateTheApp, + ExecuteAsync = () => Device.InvokeOnMainThreadAsync(() => Rate()) + }, + new SettingsPageListItem + { + Name = AppResources.DeleteAccount, + ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage())) + } }; // TODO: improve this. Leaving this as is to reduce error possibility on the hotfix. From d2fbf5bdeab5fcbad032dc9c813921f8337f48d7 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 14 Jul 2022 19:17:04 -0300 Subject: [PATCH 21/23] EC-312 Fix crash on entering invalid credentials five times on Autofill (#1988) --- src/App/Abstractions/IAccountsManager.cs | 1 + src/App/App.xaml.cs | 2 +- .../AccountManagement/AccountsManager.cs | 12 +- src/Core/Services/AuthService.cs | 3 +- .../CredentialProviderViewController.cs | 347 +++++++++++------- .../BaseLockPasswordViewController.cs | 41 +-- 6 files changed, 236 insertions(+), 170 deletions(-) diff --git a/src/App/Abstractions/IAccountsManager.cs b/src/App/Abstractions/IAccountsManager.cs index 684230d01..37acbc1db 100644 --- a/src/App/Abstractions/IAccountsManager.cs +++ b/src/App/Abstractions/IAccountsManager.cs @@ -8,5 +8,6 @@ namespace Bit.App.Abstractions { void Init(Func getOptionsFunc, IAccountsManagerHost accountsManagerHost); Task NavigateOnAccountChangeAsync(bool? isAuthed = null); + Task LogOutAsync(string userId, bool userInitiated, bool expired); } } diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 3415fd7e9..8d2b74538 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -301,7 +301,7 @@ namespace Bit.App UpdateThemeAsync(); }; Current.MainPage = new NavigationPage(new HomePage(Options)); - var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync(); + _accountsManager.NavigateOnAccountChangeAsync().FireAndForget(); ServiceContainer.Resolve("platformUtilsService").Init(); } diff --git a/src/App/Utilities/AccountManagement/AccountsManager.cs b/src/App/Utilities/AccountManagement/AccountsManager.cs index dd5b639f1..88701f2b7 100644 --- a/src/App/Utilities/AccountManagement/AccountsManager.cs +++ b/src/App/Utilities/AccountManagement/AccountsManager.cs @@ -123,7 +123,11 @@ namespace Bit.App.Utilities.AccountManagement await _vaultTimeoutService.LockAsync(true); break; case AccountsManagerMessageCommands.LOGOUT: - await Device.InvokeOnMainThreadAsync(() => LogOutAsync(message.Data as Tuple)); + var extras = message.Data as Tuple; + var userId = extras?.Item1; + var userInitiated = extras?.Item2 ?? true; + var expired = extras?.Item3 ?? false; + await Device.InvokeOnMainThreadAsync(() => LogOutAsync(userId, userInitiated, expired)); break; case AccountsManagerMessageCommands.LOGGED_OUT: // Clean up old migrated key if they ever log out. @@ -181,12 +185,8 @@ namespace Bit.App.Utilities.AccountManagement }); } - private async Task LogOutAsync(Tuple extras) + public async Task LogOutAsync(string userId, bool userInitiated, bool expired) { - var userId = extras?.Item1; - var userInitiated = extras?.Item2 ?? true; - var expired = extras?.Item3 ?? false; - await AppHelpers.LogOutAsync(userId, userInitiated); await NavigateOnAccountChangeAsync(); _authService.LogOut(() => diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index c1fb38fa8..7316a758e 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -6,6 +6,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; +using Bit.Core.Utilities; namespace Bit.Core.Services { @@ -173,7 +174,7 @@ namespace Bit.Core.Services public void LogOut(Action callback) { callback.Invoke(); - _messagingService.Send("loggedOut"); + _messagingService.Send(AccountsManagerMessageCommands.LOGGED_OUT); } public List GetSupportedTwoFactorProviders() diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index 298a6e1cb..9bd087f17 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -8,10 +8,12 @@ using Bit.App.Utilities; using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; using Bit.iOS.Autofill.Models; using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Views; +using CoreFoundation; using CoreNFC; using Foundation; using UIKit; @@ -36,88 +38,128 @@ namespace Bit.iOS.Autofill public override void ViewDidLoad() { - InitApp(); - base.ViewDidLoad(); - Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png"); - View.BackgroundColor = ThemeHelpers.SplashBackgroundColor; - _context = new Context + try { - ExtContext = ExtensionContext - }; + InitApp(); + base.ViewDidLoad(); + Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png"); + View.BackgroundColor = ThemeHelpers.SplashBackgroundColor; + _context = new Context + { + ExtContext = ExtensionContext + }; + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers) { - InitAppIfNeeded(); - _context.ServiceIdentifiers = serviceIdentifiers; - if (serviceIdentifiers.Length > 0) + try { - var uri = serviceIdentifiers[0].Identifier; - if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain) + InitAppIfNeeded(); + _context.ServiceIdentifiers = serviceIdentifiers; + if (serviceIdentifiers.Length > 0) { - uri = string.Concat("https://", uri); + var uri = serviceIdentifiers[0].Identifier; + if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain) + { + uri = string.Concat("https://", uri); + } + _context.UrlString = uri; } - _context.UrlString = uri; - } - if (!await IsAuthed()) - { - await _accountsManager.NavigateOnAccountChangeAsync(false); - } - else if (await IsLocked()) - { - PerformSegue("lockPasswordSegue", this); - } - else - { - if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) + if (!await IsAuthed()) { - PerformSegue("loginSearchSegue", this); + await _accountsManager.NavigateOnAccountChangeAsync(false); + } + else if (await IsLocked()) + { + PerformSegue("lockPasswordSegue", this); } else { - PerformSegue("loginListSegue", this); + if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) + { + PerformSegue("loginSearchSegue", this); + } + else + { + PerformSegue("loginListSegue", this); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) { - InitAppIfNeeded(); - await _stateService.Value.SetPasswordRepromptAutofillAsync(false); - await _stateService.Value.SetPasswordVerifiedAutofillAsync(false); - if (!await IsAuthed() || await IsLocked()) + try { - var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); - ExtensionContext.CancelRequest(err); - return; + InitAppIfNeeded(); + await _stateService.Value.SetPasswordRepromptAutofillAsync(false); + await _stateService.Value.SetPasswordVerifiedAutofillAsync(false); + if (!await IsAuthed() || await IsLocked()) + { + var err = new NSError(new NSString("ASExtensionErrorDomain"), + Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); + ExtensionContext.CancelRequest(err); + return; + } + _context.CredentialIdentity = credentialIdentity; + await ProvideCredentialAsync(false); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - _context.CredentialIdentity = credentialIdentity; - await ProvideCredentialAsync(false); } public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) { - InitAppIfNeeded(); - if (!await IsAuthed()) + try { - await _accountsManager.NavigateOnAccountChangeAsync(false); - return; + InitAppIfNeeded(); + if (!await IsAuthed()) + { + await _accountsManager.NavigateOnAccountChangeAsync(false); + return; + } + _context.CredentialIdentity = credentialIdentity; + await CheckLockAsync(async () => await ProvideCredentialAsync()); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - _context.CredentialIdentity = credentialIdentity; - CheckLock(async () => await ProvideCredentialAsync()); } public override async void PrepareInterfaceForExtensionConfiguration() { - InitAppIfNeeded(); - _context.Configuring = true; - if (!await IsAuthed()) + try { - await _accountsManager.NavigateOnAccountChangeAsync(false); - return; + InitAppIfNeeded(); + _context.Configuring = true; + if (!await IsAuthed()) + { + await _accountsManager.NavigateOnAccountChangeAsync(false); + return; + } + await CheckLockAsync(() => PerformSegue("setupSegue", this)); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } - CheckLock(() => PerformSegue("setupSegue", this)); } public void CompleteRequest(string id = null, string username = null, @@ -159,34 +201,43 @@ namespace Bit.iOS.Autofill public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) { - if (segue.DestinationViewController is UINavigationController navController) + try { - if (navController.TopViewController is LoginListViewController listLoginController) + if (segue.DestinationViewController is UINavigationController navController) { - listLoginController.Context = _context; - listLoginController.CPViewController = this; - segue.DestinationViewController.PresentationController.Delegate = - new CustomPresentationControllerDelegate(listLoginController.DismissModalAction); - } - else if (navController.TopViewController is LoginSearchViewController listSearchController) - { - listSearchController.Context = _context; - listSearchController.CPViewController = this; - segue.DestinationViewController.PresentationController.Delegate = - new CustomPresentationControllerDelegate(listSearchController.DismissModalAction); - } - else if (navController.TopViewController is LockPasswordViewController passwordViewController) - { - passwordViewController.CPViewController = this; - segue.DestinationViewController.PresentationController.Delegate = - new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction); - } - else if (navController.TopViewController is SetupViewController setupViewController) - { - setupViewController.CPViewController = this; - segue.DestinationViewController.PresentationController.Delegate = - new CustomPresentationControllerDelegate(setupViewController.DismissModalAction); + if (navController.TopViewController is LoginListViewController listLoginController) + { + listLoginController.Context = _context; + listLoginController.CPViewController = this; + segue.DestinationViewController.PresentationController.Delegate = + new CustomPresentationControllerDelegate(listLoginController.DismissModalAction); + } + else if (navController.TopViewController is LoginSearchViewController listSearchController) + { + listSearchController.Context = _context; + listSearchController.CPViewController = this; + segue.DestinationViewController.PresentationController.Delegate = + new CustomPresentationControllerDelegate(listSearchController.DismissModalAction); + } + else if (navController.TopViewController is LockPasswordViewController passwordViewController) + { + passwordViewController.CPViewController = this; + segue.DestinationViewController.PresentationController.Delegate = + new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction); + } + else if (navController.TopViewController is SetupViewController setupViewController) + { + setupViewController.CPViewController = this; + segue.DestinationViewController.PresentationController.Delegate = + new CustomPresentationControllerDelegate(setupViewController.DismissModalAction); + } } + + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } } @@ -194,93 +245,109 @@ namespace Bit.iOS.Autofill { DismissViewController(false, async () => { - if (_context.CredentialIdentity != null) + try { - await ProvideCredentialAsync(); - return; + if (_context.CredentialIdentity != null) + { + await ProvideCredentialAsync(); + return; + } + if (_context.Configuring) + { + PerformSegue("setupSegue", this); + return; + } + if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) + { + PerformSegue("loginSearchSegue", this); + } + else + { + PerformSegue("loginListSegue", this); + } } - if (_context.Configuring) + catch (Exception ex) { - PerformSegue("setupSegue", this); - return; - } - if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) - { - PerformSegue("loginSearchSegue", this); - } - else - { - PerformSegue("loginListSegue", this); + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; } }); } private async Task ProvideCredentialAsync(bool userInteraction = true) { - var cipherService = ServiceContainer.Resolve("cipherService", true); - Bit.Core.Models.Domain.Cipher cipher = null; - var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null; - if (!cancel) + try { - cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier); - cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null; - } - if (cancel) - { - var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null); - ExtensionContext?.CancelRequest(err); - return; - } - - var decCipher = await cipher.DecryptAsync(); - if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None) - { - // Prompt for password using either the lock screen or dialog unless - // already verified the password. - if (!userInteraction) + var cipherService = ServiceContainer.Resolve("cipherService", true); + Bit.Core.Models.Domain.Cipher cipher = null; + var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null; + if (!cancel) + { + cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier); + cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null; + } + if (cancel) { - await _stateService.Value.SetPasswordRepromptAutofillAsync(true); var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); + Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null); ExtensionContext?.CancelRequest(err); return; } - else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync()) + + var decCipher = await cipher.DecryptAsync(); + if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None) { - // Add a timeout to resolve keyboard not always showing up. - await Task.Delay(250); - var passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); - if (!await passwordRepromptService.ShowPasswordPromptAsync()) + // Prompt for password using either the lock screen or dialog unless + // already verified the password. + if (!userInteraction) { + await _stateService.Value.SetPasswordRepromptAutofillAsync(true); var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null); + Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); ExtensionContext?.CancelRequest(err); return; } + else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync()) + { + // Add a timeout to resolve keyboard not always showing up. + await Task.Delay(250); + var passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); + if (!await passwordRepromptService.ShowPasswordPromptAsync()) + { + var err = new NSError(new NSString("ASExtensionErrorDomain"), + Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null); + ExtensionContext?.CancelRequest(err); + return; + } + } } - } - string totpCode = null; - var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync(); - if (!disableTotpCopy.GetValueOrDefault(false)) - { - var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync(); - if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) && - (canAccessPremiumAsync || cipher.OrganizationUseTotp)) + string totpCode = null; + var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync(); + if (!disableTotpCopy.GetValueOrDefault(false)) { - var totpService = ServiceContainer.Resolve("totpService"); - totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp); + var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync(); + if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) && + (canAccessPremiumAsync || cipher.OrganizationUseTotp)) + { + var totpService = ServiceContainer.Resolve("totpService"); + totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp); + } } - } - CompleteRequest(decCipher.Id, decCipher.Login.Username, decCipher.Login.Password, totpCode); + CompleteRequest(decCipher.Id, decCipher.Login.Username, decCipher.Login.Password, totpCode); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + throw; + } } - private async void CheckLock(Action notLockedAction) + private async Task CheckLockAsync(Action notLockedAction) { if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync()) { - PerformSegue("lockPasswordSegue", this); + DispatchQueue.MainQueue.DispatchAsync(() => PerformSegue("lockPasswordSegue", this)); } else { @@ -303,15 +370,21 @@ namespace Bit.iOS.Autofill { NSRunLoop.Main.BeginInvokeOnMainThread(async () => { - if (await IsAuthed()) + try { - await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); - var deviceActionService = ServiceContainer.Resolve("deviceActionService"); - if (deviceActionService.SystemMajorVersion() >= 12) + if (await IsAuthed()) { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); + if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0)) + { + await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); + } } } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } }); } diff --git a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs index e4ad3248a..a0f16b6a7 100644 --- a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs @@ -28,6 +28,7 @@ namespace Bit.iOS.Core.Controllers private IPlatformUtilsService _platformUtilsService; private IBiometricService _biometricService; private IKeyConnectorService _keyConnectorService; + private IAccountsManager _accountManager; private bool _isPinProtected; private bool _isPinProtectedWithKey; private bool _pinLock; @@ -84,7 +85,7 @@ namespace Bit.iOS.Core.Controllers } public abstract UITableView TableView { get; } - + public override async void ViewDidLoad() { _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); @@ -95,6 +96,7 @@ namespace Bit.iOS.Core.Controllers _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _biometricService = ServiceContainer.Resolve("biometricService"); _keyConnectorService = ServiceContainer.Resolve("keyConnectorService"); + _accountManager = ServiceContainer.Resolve("accountsManager"); // We re-use the lock screen for autofill extension to verify master password // when trying to access protected items. @@ -265,13 +267,7 @@ namespace Bit.iOS.Core.Controllers } if (failed) { - var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); - if (invalidUnlockAttempts >= 5) - { - await LogOutAsync(); - return; - } - InvalidValue(); + await HandleFailedCredentialsAsync(); } } else @@ -306,17 +302,22 @@ namespace Bit.iOS.Core.Controllers } else { - var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); - if (invalidUnlockAttempts >= 5) - { - await LogOutAsync(); - return; - } - InvalidValue(); + await HandleFailedCredentialsAsync(); } } } + private async Task HandleFailedCredentialsAsync() + { + var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); + if (invalidUnlockAttempts >= 5) + { + await _accountManager.LogOutAsync(await _stateService.GetActiveUserIdAsync(), false, false); + return; + } + InvalidValue(); + } + public async Task PromptBiometricAsync() { if (!_biometricLock || !_biometricIntegrityValid) @@ -395,16 +396,6 @@ namespace Bit.iOS.Core.Controllers PresentViewController(alert, true, null); } - private async Task LogOutAsync() - { - await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync()); - var authService = ServiceContainer.Resolve("authService"); - authService.LogOut(() => - { - Cancel?.Invoke(); - }); - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); From c07c305384eeed49cc2e949eb26b8100df7533b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Fri, 15 Jul 2022 11:08:38 +0200 Subject: [PATCH 22/23] Add version change check in the version bump workflow (#1992) --- .github/workflows/version-bump.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 0b5c86e46..b3c5a58ef 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -19,12 +19,6 @@ jobs: - name: Create Version Branch run: | git switch -c version_bump_${{ github.event.inputs.version_number }} - git push -u origin version_bump_${{ github.event.inputs.version_number }} - - - name: Checkout Version Branch - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 - with: - ref: version_bump_${{ github.event.inputs.version_number }} - name: Bump Version - Android XML uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945 @@ -56,16 +50,32 @@ jobs: version: ${{ github.event.inputs.version_number }} file_path: "./src/iOS/Info.plist" - - name: Commit files + - name: Setup git run: | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" + + - name: Check if version changed + id: version-changed + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "::set-output name=changes_to_commit::TRUE" + else + echo "::set-output name=changes_to_commit::FALSE" + echo "No changes to commit!"; + fi + + - name: Commit files + if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} + run: | git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a - name: Push changes + if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} run: git push -u origin version_bump_${{ github.event.inputs.version_number }} - name: Create Version PR + if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} env: PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From cb0c52fb26532d52da0df2254096b1023f10a18d Mon Sep 17 00:00:00 2001 From: Micaiah Martin <77340197+mimartin12@users.noreply.github.com> Date: Fri, 15 Jul 2022 13:30:14 +0000 Subject: [PATCH 23/23] Add publish options to release workflow (#1994) --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e28ba9a8..6a90503b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,11 @@ on: - Initial Release - Redeploy - Dry Run + fdroid_publish: + description: 'Publish to f-droid store' + required: true + default: true + type: boolean jobs: release: @@ -78,6 +83,7 @@ jobs: name: F-Droid Release runs-on: ubuntu-20.04 needs: release + if: inputs.fdroid_publish steps: - name: Checkout repo uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0