From 292908f53f941907b206a12fcc4a35ef411364a5 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 12 Jul 2022 14:12:23 -0300 Subject: [PATCH] [EC-259] Added Account Switching to Share extension on iOS (#1971) * EC-259 Added Account switching on share extension on iOS, also improved performance for this and exception handling * EC-259 code formatting * EC-259 Added account switching to Share extension Send view * EC-259 Fixed navigation on share extension when a forms page is already presented * EC-259 Fix send text UI update when going from the iOS extension * EC-259 Improved DateTimeViewModel with helper property to easily setup date and time at the same time and applied on usage --- src/Android/MainApplication.cs | 5 +- src/App/App.csproj | 11 +- .../AccountViewCellViewModel.cs | 3 +- src/App/Controls/AvatarImageSource.cs | 85 +++-- src/App/Controls/AvatarImageSourcePool.cs | 33 ++ src/App/Controls/DateTime/DateTimePicker.xaml | 20 ++ .../Controls/DateTime/DateTimePicker.xaml.cs | 34 ++ .../Controls/DateTime/DateTimeViewModel.cs | 70 ++++ src/App/Pages/Send/SendAddEditPage.xaml | 10 +- src/App/Pages/Send/SendAddEditPage.xaml.cs | 10 +- .../Pages/Send/SendAddEditPageViewModel.cs | 121 +++---- .../Pages/Send/SendAddOnlyOptionsView.xaml | 183 ++++++++++ .../Pages/Send/SendAddOnlyOptionsView.xaml.cs | 91 +++++ src/App/Pages/Send/SendAddOnlyPage.xaml | 190 ++++++++++ src/App/Pages/Send/SendAddOnlyPage.xaml.cs | 178 ++++++++++ .../AccountManagement/AccountsManager.cs | 76 ++-- .../BaseLockPasswordViewController.cs | 90 +++-- .../Controllers/ExtendedUIViewController.cs | 34 +- .../Controllers/LockPasswordViewController.cs | 2 +- .../AccountSwitchingOverlayHelper.cs | 16 +- src/iOS.Core/Utilities/iOSCoreHelpers.cs | 42 ++- .../ExtensionNavigationController.cs | 27 ++ .../ExtensionNavigationController.designer.cs | 20 ++ .../LoadingViewController.cs | 333 ++++++++++-------- .../LockPasswordViewController.cs | 77 +++- .../LockPasswordViewController.designer.cs | 27 ++ .../MainInterface.storyboard | 139 +++++--- .../iOS.ShareExtension.csproj | 5 +- 28 files changed, 1509 insertions(+), 423 deletions(-) create mode 100644 src/App/Controls/AvatarImageSourcePool.cs create mode 100644 src/App/Controls/DateTime/DateTimePicker.xaml create mode 100644 src/App/Controls/DateTime/DateTimePicker.xaml.cs create mode 100644 src/App/Controls/DateTime/DateTimeViewModel.cs create mode 100644 src/App/Pages/Send/SendAddOnlyOptionsView.xaml create mode 100644 src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs create mode 100644 src/App/Pages/Send/SendAddOnlyPage.xaml create mode 100644 src/App/Pages/Send/SendAddOnlyPage.xaml.cs create mode 100644 src/iOS.ShareExtension/ExtensionNavigationController.cs create mode 100644 src/iOS.ShareExtension/ExtensionNavigationController.designer.cs diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 991aea474..bea0569ff 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -20,6 +20,7 @@ using System.Net; using Bit.App.Utilities; using Bit.App.Pages; using Bit.App.Utilities.AccountManagement; +using Bit.App.Controls; #if !FDROID using Android.Gms.Security; #endif @@ -69,7 +70,8 @@ namespace Bit.Droid ServiceContainer.Resolve("secureStorageService"), ServiceContainer.Resolve("stateService"), ServiceContainer.Resolve("platformUtilsService"), - ServiceContainer.Resolve("authService")); + ServiceContainer.Resolve("authService"), + ServiceContainer.Resolve("logger")); ServiceContainer.Register("accountsManager", accountsManager); } #if !FDROID @@ -160,6 +162,7 @@ namespace Bit.Droid ServiceContainer.Register("cryptoFunctionService", cryptoFunctionService); ServiceContainer.Register("cryptoService", cryptoService); ServiceContainer.Register("passwordRepromptService", passwordRepromptService); + ServiceContainer.Register("avatarImageSourcePool", new AvatarImageSourcePool()); // Push #if FDROID diff --git a/src/App/App.csproj b/src/App/App.csproj index a6ba8249d..25ac5fc6b 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -129,12 +129,10 @@ + - - MSBuild:UpdateDesignTimeXaml - @@ -162,12 +160,6 @@ - - - MSBuild:UpdateDesignTimeXaml - - - AppResources.cs.resx @@ -422,5 +414,6 @@ + diff --git a/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs index 3c2da33f2..e99b80f44 100644 --- a/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs +++ b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs @@ -13,7 +13,8 @@ namespace Bit.App.Controls public AccountViewCellViewModel(AccountView accountView) { AccountView = accountView; - AvatarImageSource = new AvatarImageSource(AccountView.Name, AccountView.Email); + AvatarImageSource = ServiceContainer.Resolve("avatarImageSourcePool") + ?.GetOrCreateAvatar(AccountView.Name, AccountView.Email); } public AccountView AccountView diff --git a/src/App/Controls/AvatarImageSource.cs b/src/App/Controls/AvatarImageSource.cs index fd9a86308..4b5902618 100644 --- a/src/App/Controls/AvatarImageSource.cs +++ b/src/App/Controls/AvatarImageSource.cs @@ -50,7 +50,7 @@ namespace Bit.App.Controls private Stream Draw() { - string chars = null; + string chars; string upperData = null; if (string.IsNullOrEmpty(_data)) @@ -71,43 +71,62 @@ namespace Bit.App.Controls var textColor = Color.White; var size = 50; - var bitmap = new SKBitmap( - size * 2, + using (var bitmap = new SKBitmap(size * 2, size * 2, SKImageInfo.PlatformColorType, - SKAlphaType.Premul); - var canvas = new SKCanvas(bitmap); - canvas.Clear(SKColors.Transparent); - - var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; - var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2; - var radius = midX - midX / 5; - - var circlePaint = new SKPaint + SKAlphaType.Premul)) { - IsAntialias = true, - Style = SKPaintStyle.Fill, - StrokeJoin = SKStrokeJoin.Miter, - Color = SKColor.Parse(bgColor.ToHex()) - }; - canvas.DrawCircle(midX, midY, radius, circlePaint); + using (var canvas = new SKCanvas(bitmap)) + { + canvas.Clear(SKColors.Transparent); + using (var paint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(bgColor.ToHex()) + }) + { + var midX = canvas.LocalClipBounds.Size.ToSizeI().Width / 2; + var midY = canvas.LocalClipBounds.Size.ToSizeI().Height / 2; + var radius = midX - midX / 5; - var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); - var textSize = midX / 1.3f; - var textPaint = new SKPaint - { - IsAntialias = true, - Style = SKPaintStyle.Fill, - Color = SKColor.Parse(textColor.ToHex()), - TextSize = textSize, - TextAlign = SKTextAlign.Center, - Typeface = typeface - }; - var rect = new SKRect(); - textPaint.MeasureText(chars, ref rect); - canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint); + using (var circlePaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + StrokeJoin = SKStrokeJoin.Miter, + Color = SKColor.Parse(bgColor.ToHex()) + }) + { + canvas.DrawCircle(midX, midY, radius, circlePaint); - return SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100).AsStream(); + var typeface = SKTypeface.FromFamilyName("Arial", SKFontStyle.Normal); + var textSize = midX / 1.3f; + using (var textPaint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.Fill, + Color = SKColor.Parse(textColor.ToHex()), + TextSize = textSize, + TextAlign = SKTextAlign.Center, + Typeface = typeface + }) + { + var rect = new SKRect(); + textPaint.MeasureText(chars, ref rect); + canvas.DrawText(chars, midX, midY + rect.Height / 2, textPaint); + + using (var img = SKImage.FromBitmap(bitmap)) + { + var data = img.Encode(SKEncodedImageFormat.Png, 100); + return data?.AsStream(true); + } + } + } + } + } + } } private string GetFirstLetters(string data, int charCount) diff --git a/src/App/Controls/AvatarImageSourcePool.cs b/src/App/Controls/AvatarImageSourcePool.cs new file mode 100644 index 000000000..53e463e80 --- /dev/null +++ b/src/App/Controls/AvatarImageSourcePool.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Concurrent; + +namespace Bit.App.Controls +{ + public interface IAvatarImageSourcePool + { + AvatarImageSource GetOrCreateAvatar(string name, string email); + } + + public class AvatarImageSourcePool : IAvatarImageSourcePool + { + private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + + public AvatarImageSource GetOrCreateAvatar(string name, string email) + { + var key = $"{name}{email}"; + if (!_cache.TryGetValue(key, out var avatar)) + { + avatar = new AvatarImageSource(name, email); + if (!_cache.TryAdd(key, avatar) + && + !_cache.TryGetValue(key, out avatar)) // If add fails another thread created the avatar in between the first try get and the try add. + { + // if add and get after fails, then something wrong is going on with this method. + throw new InvalidOperationException("Something is wrong creating the avatar image"); + } + } + return avatar; + } + } +} + diff --git a/src/App/Controls/DateTime/DateTimePicker.xaml b/src/App/Controls/DateTime/DateTimePicker.xaml new file mode 100644 index 000000000..fe270c6ea --- /dev/null +++ b/src/App/Controls/DateTime/DateTimePicker.xaml @@ -0,0 +1,20 @@ + + + + + diff --git a/src/App/Controls/DateTime/DateTimePicker.xaml.cs b/src/App/Controls/DateTime/DateTimePicker.xaml.cs new file mode 100644 index 000000000..8bcd5346c --- /dev/null +++ b/src/App/Controls/DateTime/DateTimePicker.xaml.cs @@ -0,0 +1,34 @@ +using System.Runtime.CompilerServices; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public partial class DateTimePicker : Grid + { + public DateTimePicker() + { + InitializeComponent(); + } + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + base.OnPropertyChanged(propertyName); + + if (propertyName == nameof(BindingContext) + && + BindingContext is DateTimeViewModel dateTimeViewModel) + { + AutomationProperties.SetName(_datePicker, dateTimeViewModel.DateName); + AutomationProperties.SetName(_timePicker, dateTimeViewModel.TimeName); + + _datePicker.PlaceHolder = dateTimeViewModel.DatePlaceholder; + _timePicker.PlaceHolder = dateTimeViewModel.TimePlaceholder; + } + } + } + + public class LazyDateTimePicker : LazyView + { + } +} diff --git a/src/App/Controls/DateTime/DateTimeViewModel.cs b/src/App/Controls/DateTime/DateTimeViewModel.cs new file mode 100644 index 000000000..ac940a773 --- /dev/null +++ b/src/App/Controls/DateTime/DateTimeViewModel.cs @@ -0,0 +1,70 @@ +using System; +using Bit.Core.Utilities; + +namespace Bit.App.Controls +{ + public class DateTimeViewModel : ExtendedViewModel + { + DateTime? _date; + TimeSpan? _time; + + public DateTimeViewModel(string dateName, string timeName) + { + DateName = dateName; + TimeName = timeName; + } + + public Action OnDateChanged { get; set; } + public Action OnTimeChanged { get; set; } + + public DateTime? Date + { + get => _date; + set + { + if (SetProperty(ref _date, value)) + { + OnDateChanged?.Invoke(value); + } + } + } + public TimeSpan? Time + { + get => _time; + set + { + if (SetProperty(ref _time, value)) + { + OnTimeChanged?.Invoke(value); + } + } + } + + public string DateName { get; } + public string TimeName { get; } + + public string DatePlaceholder { get; set; } + public string TimePlaceholder { get; set; } + + public DateTime? DateTime + { + get + { + if (Date.HasValue) + { + if (Time.HasValue) + { + return Date.Value.Add(Time.Value); + } + return Date; + } + return null; + } + set + { + Date = value?.Date; + Time = value?.Date.TimeOfDay; + } + } + } +} diff --git a/src/App/Pages/Send/SendAddEditPage.xaml b/src/App/Pages/Send/SendAddEditPage.xaml index 40e059125..018e78403 100644 --- a/src/App/Pages/Send/SendAddEditPage.xaml +++ b/src/App/Pages/Send/SendAddEditPage.xaml @@ -1,4 +1,4 @@ - + (AppResources.ThirtyDays, AppResources.ThirtyDays), new KeyValuePair(AppResources.Custom, AppResources.Custom), }; + + DeletionDateTimeViewModel = new DateTimeViewModel(AppResources.DeletionDate, AppResources.DeletionTime); + ExpirationDateTimeViewModel = new DateTimeViewModel(AppResources.ExpirationDate, AppResources.ExpirationTime) + { + OnDateChanged = date => + { + if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Time.HasValue) + { + // auto-set time to current time upon setting date + ExpirationDateTimeViewModel.Time = DateTimeNow().TimeOfDay; + } + }, + OnTimeChanged = time => + { + if (!_isOverridingPickers && !ExpirationDateTimeViewModel.Date.HasValue) + { + // auto-set date to current date upon setting time + ExpirationDateTimeViewModel.Date = DateTime.Today; + } + }, + DatePlaceholder = "mm/dd/yyyy", + TimePlaceholder = "--:-- --" + }; + + AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger); } + public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } public Command TogglePasswordCommand { get; set; } public Command ToggleOptionsCommand { get; set; } public string SendId { get; set; } @@ -126,23 +149,14 @@ namespace Bit.App.Pages } } } - public DateTime DeletionDate - { - get => _deletionDate; - set => SetProperty(ref _deletionDate, value); - } - public TimeSpan DeletionTime - { - get => _deletionTime; - set => SetProperty(ref _deletionTime, value); - } public bool ShowOptions { get => _showOptions; set => SetProperty(ref _showOptions, value, additionalPropertyNames: new[] { - nameof(OptionsAccessilibityText) + nameof(OptionsAccessilibityText), + nameof(OptionsShowHideIcon) }); } public int ExpirationDateTypeSelectedIndex @@ -156,28 +170,7 @@ namespace Bit.App.Pages } } } - public DateTime? ExpirationDate - { - get => _expirationDate; - set - { - if (SetProperty(ref _expirationDate, value)) - { - ExpirationDateChanged(); - } - } - } - public TimeSpan? ExpirationTime - { - get => _expirationTime; - set - { - if (SetProperty(ref _expirationTime, value)) - { - ExpirationTimeChanged(); - } - } - } + public int? MaxAccessCount { get => _maxAccessCount; @@ -205,7 +198,7 @@ namespace Bit.App.Pages } public string FileName { - get => _fileName; + get => _fileName ?? AppResources.NoFileChosen; set { if (SetProperty(ref _fileName, value)) @@ -240,10 +233,13 @@ namespace Bit.App.Pages public bool IsFile => Send?.Type == SendType.File; public bool ShowDeletionCustomPickers => EditMode || DeletionDateTypeSelectedIndex == 6; public bool ShowExpirationCustomPickers => EditMode || ExpirationDateTypeSelectedIndex == 7; + public DateTimeViewModel DeletionDateTimeViewModel { get; } + public DateTimeViewModel ExpirationDateTimeViewModel { get; } public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public string FileTypeAccessibilityLabel => IsFile ? AppResources.FileTypeIsSelected : AppResources.FileTypeIsNotSelected; public string TextTypeAccessibilityLabel => IsText ? AppResources.TextTypeIsSelected : AppResources.TextTypeIsNotSelected; + public string OptionsShowHideIcon => ShowOptions ? BitwardenIcons.ChevronUp : BitwardenIcons.AngleDown; public async Task InitAsync() { @@ -268,10 +264,8 @@ namespace Bit.App.Pages return false; } Send = await send.DecryptAsync(); - DeletionDate = Send.DeletionDate.ToLocalTime(); - DeletionTime = DeletionDate.TimeOfDay; - ExpirationDate = Send.ExpirationDate?.ToLocalTime(); - ExpirationTime = ExpirationDate?.TimeOfDay; + DeletionDateTimeViewModel.DateTime = Send.DeletionDate.ToLocalTime(); + ExpirationDateTimeViewModel.DateTime = Send.ExpirationDate?.ToLocalTime(); } else { @@ -280,8 +274,7 @@ namespace Bit.App.Pages { Type = Type.GetValueOrDefault(defaultType), }; - _deletionDate = DateTimeNow().AddDays(7); - _deletionTime = DeletionDate.TimeOfDay; + DeletionDateTimeViewModel.DateTime = DateTimeNow().AddDays(7); DeletionDateTypeSelectedIndex = 4; ExpirationDateTypeSelectedIndex = 0; } @@ -305,23 +298,22 @@ namespace Bit.App.Pages public void ClearExpirationDate() { _isOverridingPickers = true; - ExpirationDate = null; - ExpirationTime = null; + ExpirationDateTimeViewModel.DateTime = null; _isOverridingPickers = false; } private void UpdateSendData() { // filename - if (Send.File != null && FileName != null) + if (Send.File != null && _fileName != null) { - Send.File.FileName = FileName; + Send.File.FileName = _fileName; } // deletion date if (ShowDeletionCustomPickers) { - Send.DeletionDate = DeletionDate.Date.Add(DeletionTime).ToUniversalTime(); + Send.DeletionDate = DeletionDateTimeViewModel.DateTime.Value.ToUniversalTime(); } else { @@ -329,9 +321,9 @@ namespace Bit.App.Pages } // expiration date - if (ShowExpirationCustomPickers && ExpirationDate.HasValue && ExpirationTime.HasValue) + if (ShowExpirationCustomPickers && ExpirationDateTimeViewModel.DateTime.HasValue) { - Send.ExpirationDate = ExpirationDate.Value.Date.Add(ExpirationTime.Value).ToUniversalTime(); + Send.ExpirationDate = ExpirationDateTimeViewModel.DateTime.Value.ToUniversalTime(); } else if (_simpleExpirationDateTime.HasValue) { @@ -484,7 +476,7 @@ namespace Bit.App.Pages return; } - if (Page is SendAddEditPage sendPage && sendPage.OnClose != null) + if (Page is SendAddOnlyPage sendPage && sendPage.OnClose != null) { sendPage.OnClose(); return; @@ -625,24 +617,6 @@ namespace Bit.App.Pages } } - private void ExpirationDateChanged() - { - if (!_isOverridingPickers && !ExpirationTime.HasValue) - { - // auto-set time to current time upon setting date - ExpirationTime = DateTimeNow().TimeOfDay; - } - } - - private void ExpirationTimeChanged() - { - if (!_isOverridingPickers && !ExpirationDate.HasValue) - { - // auto-set date to current date upon setting time - ExpirationDate = DateTime.Today; - } - } - private void MaxAccessCountChanged() { Send.MaxAccessCount = _maxAccessCount; @@ -666,5 +640,10 @@ namespace Bit.App.Pages DateTimeKind.Local ); } + + internal void TriggerSendTextPropertyChanged() + { + Device.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(Send))); + } } } diff --git a/src/App/Pages/Send/SendAddOnlyOptionsView.xaml b/src/App/Pages/Send/SendAddOnlyOptionsView.xaml new file mode 100644 index 000000000..39528c14c --- /dev/null +++ b/src/App/Pages/Send/SendAddOnlyOptionsView.xaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs b/src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs new file mode 100644 index 000000000..84829ea23 --- /dev/null +++ b/src/App/Pages/Send/SendAddOnlyOptionsView.xaml.cs @@ -0,0 +1,91 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Bit.App.Behaviors; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class SendAddOnlyOptionsView : ContentView + { + public SendAddOnlyOptionsView() + { + InitializeComponent(); + } + + private SendAddEditPageViewModel ViewModel => BindingContext as SendAddEditPageViewModel; + + public void SetMainScrollView(ScrollView scrollView) + { + _notesEditor.Behaviors.Add(new EditorPreventAutoBottomScrollingOnFocusedBehavior { ParentScrollView = scrollView }); + } + + private void OnMaxAccessCountTextChanged(object sender, TextChangedEventArgs e) + { + if (ViewModel is null) + { + return; + } + + if (string.IsNullOrWhiteSpace(e.NewTextValue)) + { + ViewModel.MaxAccessCount = null; + _maxAccessCountStepper.Value = 0; + return; + } + // accept only digits + if (!int.TryParse(e.NewTextValue, out int _)) + { + ((Entry)sender).Text = e.OldTextValue; + } + } + + protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + base.OnPropertyChanged(propertyName); + + if (propertyName == nameof(BindingContext) + && + ViewModel != null) + { + ViewModel.PropertyChanged += ViewModel_PropertyChanged; + } + } + + private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (!_lazyDeletionDateTimePicker.IsLoaded + && + e.PropertyName == nameof(SendAddEditPageViewModel.ShowDeletionCustomPickers) + && + ViewModel.ShowDeletionCustomPickers) + { + _lazyDeletionDateTimePicker.LoadViewAsync(); + } + + if (!_lazyExpirationDateTimePicker.IsLoaded + && + e.PropertyName == nameof(SendAddEditPageViewModel.ShowExpirationCustomPickers) + && + ViewModel.ShowExpirationCustomPickers) + { + _lazyExpirationDateTimePicker.LoadViewAsync(); + } + } + } + + public class SendAddOnlyOptionsLazyView : LazyView + { + public ScrollView MainScrollView { get; set; } + + public override async ValueTask LoadViewAsync() + { + await base.LoadViewAsync(); + + if (Content is SendAddOnlyOptionsView optionsView) + { + optionsView.SetMainScrollView(MainScrollView); + } + } + } +} diff --git a/src/App/Pages/Send/SendAddOnlyPage.xaml b/src/App/Pages/Send/SendAddOnlyPage.xaml new file mode 100644 index 000000000..d3a0106ff --- /dev/null +++ b/src/App/Pages/Send/SendAddOnlyPage.xaml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 +