mirror of
https://github.com/bitwarden/mobile
synced 2025-12-21 02:33:36 +00:00
[PM-5154] Implement combined view for passwords and passkeys on iOS Autofill extension (#3075)
* PM-5154 Implemented combined view of passwords and passkeys and improved search and items UI * PM-5154 Code improvement from PR feedback * PM-5154 Code improvement to log unknown exceptions
This commit is contained in:
committed by
GitHub
parent
53aedea93a
commit
144fc7c727
@@ -5407,6 +5407,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Passwords.
|
||||||
|
/// </summary>
|
||||||
|
public static string Passwords {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Passwords", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use..
|
/// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2936,4 +2936,7 @@ Do you want to switch to this account?</value>
|
|||||||
<value>There was a problem reading your passkey for {0}. Try again later.</value>
|
<value>There was a problem reading your passkey for {0}. Try again later.</value>
|
||||||
<comment>The parameter is the RpId</comment>
|
<comment>The parameter is the RpId</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Passwords" xml:space="preserve">
|
||||||
|
<value>Passwords</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -109,8 +109,9 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
throw new UnknownError();
|
throw new UnknownError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,8 +204,9 @@ namespace Bit.Core.Services
|
|||||||
Signature = signature
|
Signature = signature
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
throw new UnknownError();
|
throw new UnknownError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,25 @@ namespace Bit.iOS.Autofill
|
|||||||
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||||
private readonly LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
private readonly LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||||
private readonly LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
private readonly LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||||
|
|
||||||
|
[Export("prepareCredentialListForServiceIdentifiers:requestParameters:")]
|
||||||
|
public override void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0) && !string.IsNullOrEmpty(requestParameters?.RelyingPartyIdentifier))
|
||||||
|
{
|
||||||
|
_context.PasskeyCredentialRequestParameters = requestParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareCredentialList(serviceIdentifiers);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnProvidingCredentialException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest)
|
public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest)
|
||||||
{
|
{
|
||||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
@@ -239,7 +257,7 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference)
|
internal async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,56 +88,13 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
if (_context.IsCreatingOrPreparingListForPasskey || _context.ServiceIdentifiers?.Length > 0)
|
||||||
{
|
|
||||||
PerformSegue(SegueConstants.LOGIN_SEARCH, this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
OnProvidingCredentialException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Export("prepareCredentialListForServiceIdentifiers:requestParameters:")]
|
|
||||||
public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InitAppIfNeeded();
|
|
||||||
_context.VaultUnlockedDuringThisSession = false;
|
|
||||||
_context.ServiceIdentifiers = serviceIdentifiers;
|
|
||||||
if (serviceIdentifiers.Length > 0)
|
|
||||||
{
|
|
||||||
var uri = serviceIdentifiers[0].Identifier;
|
|
||||||
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain)
|
|
||||||
{
|
|
||||||
uri = string.Concat("https://", uri);
|
|
||||||
}
|
|
||||||
_context.UrlString = uri;
|
|
||||||
}
|
|
||||||
if (!await IsAuthed())
|
|
||||||
{
|
|
||||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
|
||||||
}
|
|
||||||
else if (await IsLocked())
|
|
||||||
{
|
|
||||||
PerformSegue(SegueConstants.LOCK, this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
|
||||||
{
|
|
||||||
PerformSegue(SegueConstants.LOGIN_SEARCH, this);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
PerformSegue(SegueConstants.LOGIN_SEARCH, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,6 +263,8 @@ namespace Bit.iOS.Autofill
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_context.PickCredentialForFido2GetAssertionFromListTcs?.TrySetCanceled();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(totp))
|
if (!string.IsNullOrWhiteSpace(totp))
|
||||||
{
|
{
|
||||||
UIPasteboard.General.String = totp;
|
UIPasteboard.General.String = totp;
|
||||||
@@ -324,7 +283,7 @@ namespace Bit.iOS.Autofill
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnProvidingCredentialException(Exception ex)
|
internal void OnProvidingCredentialException(Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
CancelRequest(ASExtensionErrorCode.Failed);
|
CancelRequest(ASExtensionErrorCode.Failed);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Bit.iOS.Autofill.Models;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.iOS.Autofill.Models;
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill
|
namespace Bit.iOS.Autofill
|
||||||
{
|
{
|
||||||
@@ -6,5 +7,8 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
Context Context { get; }
|
Context Context { get; }
|
||||||
CredentialProviderViewController CPViewController { get; }
|
CredentialProviderViewController CPViewController { get; }
|
||||||
|
void OnItemsLoaded(string searchFilter);
|
||||||
|
Task ReloadItemsAsync();
|
||||||
|
void ReloadTableViewData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AuthenticationServices;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@@ -15,8 +16,8 @@ using Bit.iOS.Core.Controllers;
|
|||||||
using Bit.iOS.Core.Utilities;
|
using Bit.iOS.Core.Utilities;
|
||||||
using Bit.iOS.Core.Views;
|
using Bit.iOS.Core.Views;
|
||||||
using CoreFoundation;
|
using CoreFoundation;
|
||||||
using CoreGraphics;
|
|
||||||
using Foundation;
|
using Foundation;
|
||||||
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill
|
namespace Bit.iOS.Autofill
|
||||||
@@ -45,8 +46,30 @@ namespace Bit.iOS.Autofill
|
|||||||
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||||
LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||||
|
LazyResolve<IFido2AuthenticatorService> _fido2AuthenticatorService = new LazyResolve<IFido2AuthenticatorService>();
|
||||||
|
|
||||||
bool _alreadyLoadItemsOnce = false;
|
bool _alreadyLoadItemsOnce = false;
|
||||||
|
bool _isLoading;
|
||||||
|
|
||||||
|
private string NavTitle
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Context.IsCreatingPasskey)
|
||||||
|
{
|
||||||
|
return AppResources.SavePasskey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Context.IsCreatingOrPreparingListForPasskey)
|
||||||
|
{
|
||||||
|
return AppResources.Autofill;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppResources.Items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableSource Source => (TableSource)TableView.Source;
|
||||||
|
|
||||||
public async override void ViewDidLoad()
|
public async override void ViewDidLoad()
|
||||||
{
|
{
|
||||||
@@ -58,16 +81,22 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
SubscribeSyncCompleted();
|
SubscribeSyncCompleted();
|
||||||
|
|
||||||
NavItem.Title = Context.IsCreatingPasskey ? AppResources.SavePasskey : AppResources.Items;
|
NavItem.Title = NavTitle;
|
||||||
|
|
||||||
_cancelButton.Title = AppResources.Cancel;
|
_cancelButton.Title = AppResources.Cancel;
|
||||||
|
|
||||||
|
_searchBar.Placeholder = AppResources.Search;
|
||||||
|
_searchBar.BackgroundColor = _searchBar.BarTintColor = ThemeHelpers.ListHeaderBackgroundColor;
|
||||||
|
_searchBar.UpdateThemeIfNeeded();
|
||||||
|
_searchBar.Delegate = new ExtensionSearchDelegate(TableView);
|
||||||
|
|
||||||
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||||
|
|
||||||
var tableSource = new TableSource(this);
|
var tableSource = new TableSource(this);
|
||||||
TableView.Source = tableSource;
|
TableView.Source = tableSource;
|
||||||
tableSource.RegisterTableViewCells(TableView);
|
tableSource.RegisterTableViewCells(TableView);
|
||||||
|
|
||||||
if (Context.IsCreatingPasskey)
|
if (Context.IsCreatingOrPreparingListForPasskey)
|
||||||
{
|
{
|
||||||
TableView.SectionHeaderHeight = 55;
|
TableView.SectionHeaderHeight = 55;
|
||||||
TableView.RegisterClassForHeaderFooterViewReuse(typeof(HeaderItemView), HEADER_SECTION_IDENTIFIER);
|
TableView.RegisterClassForHeaderFooterViewReuse(typeof(HeaderItemView), HEADER_SECTION_IDENTIFIER);
|
||||||
@@ -78,8 +107,6 @@ namespace Bit.iOS.Autofill
|
|||||||
TableView.SectionHeaderTopPadding = 0;
|
TableView.SectionHeaderTopPadding = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ((TableSource)TableView.Source).LoadAsync();
|
|
||||||
|
|
||||||
if (Context.IsCreatingPasskey)
|
if (Context.IsCreatingPasskey)
|
||||||
{
|
{
|
||||||
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, Context.UrlString);
|
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, Context.UrlString);
|
||||||
@@ -91,8 +118,6 @@ namespace Bit.iOS.Autofill
|
|||||||
_emptyViewButton.ClipsToBounds = true;
|
_emptyViewButton.ClipsToBounds = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_alreadyLoadItemsOnce = true;
|
|
||||||
|
|
||||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||||
var needsAutofillReplacement = await storageService.GetAsync<bool?>(
|
var needsAutofillReplacement = await storageService.GetAsync<bool?>(
|
||||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||||
@@ -113,6 +138,22 @@ namespace Bit.iOS.Autofill
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||||
|
|
||||||
|
if (Context.IsPreparingListForPasskey)
|
||||||
|
{
|
||||||
|
var fido2UserInterface = new Fido2GetAssertionFromListUserInterface(Context,
|
||||||
|
() => Task.CompletedTask,
|
||||||
|
() => Context?.VaultUnlockedDuringThisSession ?? false,
|
||||||
|
CPViewController.VerifyUserAsync,
|
||||||
|
Source.ReloadWithAllowedFido2Credentials);
|
||||||
|
|
||||||
|
DoFido2GetAssertionAsync(fido2UserInterface).FireAndForget();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ReloadItemsAsync();
|
||||||
|
_alreadyLoadItemsOnce = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -120,6 +161,74 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DoFido2GetAssertionAsync(IFido2GetAssertionUserInterface fido2GetAssertionUserInterface)
|
||||||
|
{
|
||||||
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
CPViewController.OnProvidingCredentialException(new InvalidOperationException("Trying to get assertion request before iOS 17"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Context.PasskeyCredentialRequestParameters is null)
|
||||||
|
{
|
||||||
|
CPViewController.OnProvidingCredentialException(new InvalidOperationException("Trying to get assertion request without a PasskeyCredentialRequestParameters"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fido2AssertionResult = await _fido2AuthenticatorService.Value.GetAssertionAsync(new Fido2AuthenticatorGetAssertionParams
|
||||||
|
{
|
||||||
|
RpId = Context.PasskeyCredentialRequestParameters.RelyingPartyIdentifier,
|
||||||
|
Hash = Context.PasskeyCredentialRequestParameters.ClientDataHash.ToArray(),
|
||||||
|
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(Context.PasskeyCredentialRequestParameters.UserVerificationPreference),
|
||||||
|
AllowCredentialDescriptorList = Context.PasskeyCredentialRequestParameters.AllowedCredentials?
|
||||||
|
.Select(c => new PublicKeyCredentialDescriptor { Id = c.ToArray() })
|
||||||
|
.ToArray()
|
||||||
|
}, fido2GetAssertionUserInterface);
|
||||||
|
|
||||||
|
if (fido2AssertionResult.SelectedCredential is null)
|
||||||
|
{
|
||||||
|
throw new NullReferenceException("SelectedCredential must have a value");
|
||||||
|
}
|
||||||
|
|
||||||
|
await CPViewController.CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
||||||
|
NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle),
|
||||||
|
Context.PasskeyCredentialRequestParameters.RelyingPartyIdentifier,
|
||||||
|
NSData.FromArray(fido2AssertionResult.Signature),
|
||||||
|
Context.PasskeyCredentialRequestParameters.ClientDataHash,
|
||||||
|
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
|
||||||
|
NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
catch (InvalidOperationNeedsUIException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Context?.IsExecutingWithoutUserInteraction == false)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.Value.ShowDialogAsync(
|
||||||
|
string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, Context.PasskeyCredentialRequestParameters.RelyingPartyIdentifier),
|
||||||
|
AppResources.ErrorReadingPasskey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelButton_TouchUpInside(object sender, EventArgs e)
|
private void CancelButton_TouchUpInside(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Cancel();
|
Cancel();
|
||||||
@@ -142,7 +251,54 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
partial void SearchBarButton_Activated(UIBarButtonItem sender)
|
partial void SearchBarButton_Activated(UIBarButtonItem sender)
|
||||||
{
|
{
|
||||||
PerformSegue(SegueConstants.LOGIN_SEARCH_FROM_LIST, this);
|
try
|
||||||
|
{
|
||||||
|
if (!Context.IsCreatingOrPreparingListForPasskey)
|
||||||
|
{
|
||||||
|
PerformSegue(SegueConstants.LOGIN_SEARCH_FROM_LIST, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isLoading)
|
||||||
|
{
|
||||||
|
// if it's loading we simplify this logic to just avoid toggling the search bar visibility
|
||||||
|
// and reloading items while this is taking place.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.Animate(0.3f,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_tableViewTopToSearchBarConstraint.Active = !_tableViewTopToSearchBarConstraint.Active;
|
||||||
|
_searchBar.Hidden = !_searchBar.Hidden;
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (_tableViewTopToSearchBarConstraint.Active)
|
||||||
|
{
|
||||||
|
_searchBar?.BecomeFirstResponder();
|
||||||
|
|
||||||
|
if (Context.IsCreatingPasskey)
|
||||||
|
{
|
||||||
|
_emptyView.Hidden = true;
|
||||||
|
TableView.Hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_searchBar.Text = string.Empty;
|
||||||
|
_searchBar.Text = null;
|
||||||
|
|
||||||
|
_searchBar.ResignFirstResponder();
|
||||||
|
|
||||||
|
ReloadItemsAsync().FireAndForget();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void EmptyButton_Activated(UIButton sender)
|
partial void EmptyButton_Activated(UIButton sender)
|
||||||
@@ -250,8 +406,7 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ((TableSource)TableView.Source).LoadAsync();
|
await ReloadItemsAsync();
|
||||||
TableView.ReloadData();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -262,10 +417,18 @@ namespace Bit.iOS.Autofill
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnEmptyList()
|
public void OnItemsLoaded(string searchFilter)
|
||||||
{
|
{
|
||||||
_emptyView.Hidden = false;
|
if (Context.IsCreatingPasskey)
|
||||||
TableView.Hidden = true;
|
{
|
||||||
|
_emptyView.Hidden = !Source.IsEmpty;
|
||||||
|
TableView.Hidden = Source.IsEmpty;
|
||||||
|
|
||||||
|
if (Source.IsEmpty)
|
||||||
|
{
|
||||||
|
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, string.IsNullOrEmpty(searchFilter) ? Context.UrlString : searchFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ViewDidUnload()
|
public override void ViewDidUnload()
|
||||||
@@ -281,8 +444,7 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ((TableSource)TableView.Source).LoadAsync();
|
await ReloadItemsAsync();
|
||||||
TableView.ReloadData();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -306,6 +468,53 @@ namespace Bit.iOS.Autofill
|
|||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadSourceAsync()
|
||||||
|
{
|
||||||
|
_isLoading = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||||
|
{
|
||||||
|
TableView.Hidden = true;
|
||||||
|
_searchBar.Hidden = true;
|
||||||
|
_loadingView.Hidden = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Source.LoadAsync(string.IsNullOrEmpty(_searchBar?.Text), _searchBar?.Text);
|
||||||
|
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||||
|
{
|
||||||
|
_loadingView.Hidden = true;
|
||||||
|
TableView.Hidden = Context.IsCreatingPasskey && Source.IsEmpty;
|
||||||
|
_searchBar.Hidden = string.IsNullOrEmpty(_searchBar?.Text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReloadItemsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LoadSourceAsync();
|
||||||
|
|
||||||
|
_alreadyLoadItemsOnce = true;
|
||||||
|
|
||||||
|
await MainThread.InvokeOnMainThreadAsync(TableView.ReloadData);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred).FireAndForget();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReloadTableViewData() => TableView.ReloadData();
|
||||||
|
|
||||||
public class TableSource : BaseLoginListTableSource<LoginListViewController>
|
public class TableSource : BaseLoginListTableSource<LoginListViewController>
|
||||||
{
|
{
|
||||||
public TableSource(LoginListViewController controller)
|
public TableSource(LoginListViewController controller)
|
||||||
@@ -314,54 +523,6 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN;
|
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN;
|
||||||
|
|
||||||
public override async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await base.LoadAsync(urlFilter, searchFilter);
|
|
||||||
|
|
||||||
if (Context.IsCreatingPasskey && !Items.Any())
|
|
||||||
{
|
|
||||||
Controller?.OnEmptyList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override UIView GetViewForHeader(UITableView tableView, nint section)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Context.IsCreatingPasskey
|
|
||||||
&&
|
|
||||||
tableView.DequeueReusableHeaderFooterView(LoginListViewController.HEADER_SECTION_IDENTIFIER) is HeaderItemView headerItemView)
|
|
||||||
{
|
|
||||||
headerItemView.SetHeaderText(AppResources.ChooseALoginToSaveThisPasskeyTo);
|
|
||||||
return headerItemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new UIView(CGRect.Empty);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
||||||
return new UIView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override nint RowsInSection(UITableView tableview, nint section)
|
|
||||||
{
|
|
||||||
if (Context.IsCreatingPasskey)
|
|
||||||
{
|
|
||||||
return Items?.Count() ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.RowsInSection(tableview, section);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/iOS.Autofill/LoginListViewController.designer.cs
generated
24
src/iOS.Autofill/LoginListViewController.designer.cs
generated
@@ -24,6 +24,15 @@ namespace Bit.iOS.Autofill
|
|||||||
[Outlet]
|
[Outlet]
|
||||||
UIKit.UILabel _emptyViewLabel { get; set; }
|
UIKit.UILabel _emptyViewLabel { get; set; }
|
||||||
|
|
||||||
|
[Outlet]
|
||||||
|
UIKit.UIActivityIndicatorView _loadingView { get; set; }
|
||||||
|
|
||||||
|
[Outlet]
|
||||||
|
UIKit.UISearchBar _searchBar { get; set; }
|
||||||
|
|
||||||
|
[Outlet]
|
||||||
|
UIKit.NSLayoutConstraint _tableViewTopToSearchBarConstraint { get; set; }
|
||||||
|
|
||||||
[Outlet]
|
[Outlet]
|
||||||
[GeneratedCode ("iOS Designer", "1.0")]
|
[GeneratedCode ("iOS Designer", "1.0")]
|
||||||
UIKit.UIBarButtonItem AddBarButton { get; set; }
|
UIKit.UIBarButtonItem AddBarButton { get; set; }
|
||||||
@@ -72,6 +81,16 @@ namespace Bit.iOS.Autofill
|
|||||||
_emptyViewLabel = null;
|
_emptyViewLabel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_searchBar != null) {
|
||||||
|
_searchBar.Dispose ();
|
||||||
|
_searchBar = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_tableViewTopToSearchBarConstraint != null) {
|
||||||
|
_tableViewTopToSearchBarConstraint.Dispose ();
|
||||||
|
_tableViewTopToSearchBarConstraint = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (AddBarButton != null) {
|
if (AddBarButton != null) {
|
||||||
AddBarButton.Dispose ();
|
AddBarButton.Dispose ();
|
||||||
AddBarButton = null;
|
AddBarButton = null;
|
||||||
@@ -96,6 +115,11 @@ namespace Bit.iOS.Autofill
|
|||||||
TableView.Dispose ();
|
TableView.Dispose ();
|
||||||
TableView = null;
|
TableView = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_loadingView != null) {
|
||||||
|
_loadingView.Dispose ();
|
||||||
|
_loadingView = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.iOS.Autofill.Models;
|
using Bit.iOS.Autofill.Models;
|
||||||
|
using Bit.iOS.Autofill.Utilities;
|
||||||
|
using Bit.iOS.Core.Controllers;
|
||||||
|
using Bit.iOS.Core.Utilities;
|
||||||
|
using Bit.iOS.Core.Views;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
using Bit.iOS.Core.Controllers;
|
|
||||||
using Bit.Core.Resources.Localization;
|
|
||||||
using Bit.iOS.Core.Views;
|
|
||||||
using Bit.iOS.Autofill.Utilities;
|
|
||||||
using Bit.iOS.Core.Utilities;
|
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill
|
namespace Bit.iOS.Autofill
|
||||||
{
|
{
|
||||||
@@ -26,22 +26,35 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
public async override void ViewDidLoad()
|
public async override void ViewDidLoad()
|
||||||
{
|
{
|
||||||
base.ViewDidLoad();
|
try
|
||||||
NavItem.Title = AppResources.SearchVault;
|
{
|
||||||
CancelBarButton.Title = AppResources.Cancel;
|
base.ViewDidLoad();
|
||||||
SearchBar.Placeholder = AppResources.Search;
|
|
||||||
SearchBar.BackgroundColor = SearchBar.BarTintColor = ThemeHelpers.ListHeaderBackgroundColor;
|
|
||||||
SearchBar.UpdateThemeIfNeeded();
|
|
||||||
|
|
||||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
NavItem.Title = AppResources.SearchVault;
|
||||||
TableView.EstimatedRowHeight = 44;
|
CancelBarButton.Title = AppResources.Cancel;
|
||||||
|
SearchBar.Placeholder = AppResources.Search;
|
||||||
var tableSource = new TableSource(this);
|
SearchBar.BackgroundColor = SearchBar.BarTintColor = ThemeHelpers.ListHeaderBackgroundColor;
|
||||||
TableView.Source = tableSource;
|
SearchBar.UpdateThemeIfNeeded();
|
||||||
tableSource.RegisterTableViewCells(TableView);
|
|
||||||
|
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||||
SearchBar.Delegate = new ExtensionSearchDelegate(TableView);
|
TableView.EstimatedRowHeight = 55;
|
||||||
await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
|
||||||
|
var tableSource = new TableSource(this);
|
||||||
|
TableView.Source = tableSource;
|
||||||
|
tableSource.RegisterTableViewCells(TableView);
|
||||||
|
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(15, 0))
|
||||||
|
{
|
||||||
|
TableView.SectionHeaderTopPadding = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchBar.Delegate = new ExtensionSearchDelegate(TableView);
|
||||||
|
await ReloadItemsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ViewDidAppear(bool animated)
|
public override void ViewDidAppear(bool animated)
|
||||||
@@ -90,11 +103,20 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
DismissViewController(true, async () =>
|
DismissViewController(true, async () =>
|
||||||
{
|
{
|
||||||
await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
await ReloadItemsAsync();
|
||||||
TableView.ReloadData();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnItemsLoaded(string searchFilter) { }
|
||||||
|
|
||||||
|
public async Task ReloadItemsAsync()
|
||||||
|
{
|
||||||
|
await((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
||||||
|
TableView.ReloadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReloadTableViewData() => TableView.ReloadData();
|
||||||
|
|
||||||
public class TableSource : BaseLoginListTableSource<LoginSearchViewController>
|
public class TableSource : BaseLoginListTableSource<LoginSearchViewController>
|
||||||
{
|
{
|
||||||
public TableSource(LoginSearchViewController controller)
|
public TableSource(LoginSearchViewController controller)
|
||||||
|
|||||||
@@ -132,6 +132,13 @@
|
|||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="830"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="830"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
|
<searchBar hidden="YES" contentMode="redraw" translatesAutoresizingMaskIntoConstraints="NO" id="FnG-4H-B7H">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="56"/>
|
||||||
|
<textInputTraits key="textInputTraits"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="2304" id="9s5-am-0Sm"/>
|
||||||
|
</connections>
|
||||||
|
</searchBar>
|
||||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wNm-Sy-bJv" userLabel="EmptyView">
|
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wNm-Sy-bJv" userLabel="EmptyView">
|
||||||
<rect key="frame" x="0.0" y="200" width="414" height="228"/>
|
<rect key="frame" x="0.0" y="200" width="414" height="228"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -172,7 +179,10 @@
|
|||||||
<constraint firstItem="FDN-Dp-jl3" firstAttribute="top" secondItem="wNm-Sy-bJv" secondAttribute="top" id="kKX-UE-JzG"/>
|
<constraint firstItem="FDN-Dp-jl3" firstAttribute="top" secondItem="wNm-Sy-bJv" secondAttribute="top" id="kKX-UE-JzG"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<tableView opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="2305">
|
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" animating="YES" style="large" translatesAutoresizingMaskIntoConstraints="NO" id="1MY-io-Uh6">
|
||||||
|
<rect key="frame" x="188.5" y="70" width="37" height="37"/>
|
||||||
|
</activityIndicatorView>
|
||||||
|
<tableView hidden="YES" opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="2305">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="781"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="781"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
@@ -219,14 +229,25 @@
|
|||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="Tq0-Ep-tHr" secondAttribute="trailing" id="5BV-0y-vU1"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="Tq0-Ep-tHr" secondAttribute="trailing" id="5BV-0y-vU1"/>
|
||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="2305" secondAttribute="bottom" id="6EB-rh-lLS"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="2305" secondAttribute="bottom" id="6EB-rh-lLS"/>
|
||||||
<constraint firstItem="wNm-Sy-bJv" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" constant="200" id="CWX-uT-sfH"/>
|
<constraint firstItem="wNm-Sy-bJv" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" constant="200" id="CWX-uT-sfH"/>
|
||||||
|
<constraint firstItem="FnG-4H-B7H" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="DyG-zK-MDg"/>
|
||||||
|
<constraint firstItem="1MY-io-Uh6" firstAttribute="centerX" secondItem="BQW-dG-XMM" secondAttribute="centerX" id="E6j-d5-Slg"/>
|
||||||
<constraint firstItem="wNm-Sy-bJv" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="Ytw-kT-KUB"/>
|
<constraint firstItem="wNm-Sy-bJv" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="Ytw-kT-KUB"/>
|
||||||
<constraint firstItem="Tq0-Ep-tHr" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="eT6-Bv-JaR"/>
|
<constraint firstItem="Tq0-Ep-tHr" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="eT6-Bv-JaR"/>
|
||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="2305" secondAttribute="trailing" id="ofJ-fL-adF"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="2305" secondAttribute="trailing" id="ofJ-fL-adF"/>
|
||||||
|
<constraint firstItem="FnG-4H-B7H" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="oz8-5S-g3r"/>
|
||||||
|
<constraint firstItem="1MY-io-Uh6" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" constant="70" id="p2k-Pz-j9M"/>
|
||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="Tq0-Ep-tHr" secondAttribute="bottom" id="pBa-o1-Mtx"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="Tq0-Ep-tHr" secondAttribute="bottom" id="pBa-o1-Mtx"/>
|
||||||
<constraint firstItem="2305" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="pGe-1e-B4s"/>
|
<constraint firstItem="2305" firstAttribute="top" secondItem="FnG-4H-B7H" secondAttribute="bottom" priority="701" id="pGe-1e-B4s"/>
|
||||||
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="FnG-4H-B7H" secondAttribute="trailing" id="qJE-bG-L66"/>
|
||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="wNm-Sy-bJv" secondAttribute="trailing" id="v0x-aS-ymc"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="wNm-Sy-bJv" secondAttribute="trailing" id="v0x-aS-ymc"/>
|
||||||
|
<constraint firstItem="2305" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" priority="700" id="wML-RE-8T6"/>
|
||||||
<constraint firstItem="2305" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="xfQ-VQ-yWe"/>
|
<constraint firstItem="2305" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="xfQ-VQ-yWe"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
|
<variation key="default">
|
||||||
|
<mask key="constraints">
|
||||||
|
<exclude reference="pGe-1e-B4s"/>
|
||||||
|
</mask>
|
||||||
|
</variation>
|
||||||
</view>
|
</view>
|
||||||
<toolbarItems/>
|
<toolbarItems/>
|
||||||
<navigationItem key="navigationItem" title="Logins" id="3734">
|
<navigationItem key="navigationItem" title="Logins" id="3734">
|
||||||
@@ -256,6 +277,9 @@
|
|||||||
<outlet property="_emptyViewButton" destination="Gv5-Xt-G9l" id="JHd-sV-VJC"/>
|
<outlet property="_emptyViewButton" destination="Gv5-Xt-G9l" id="JHd-sV-VJC"/>
|
||||||
<outlet property="_emptyViewImage" destination="FDN-Dp-jl3" id="Dzb-p3-tv0"/>
|
<outlet property="_emptyViewImage" destination="FDN-Dp-jl3" id="Dzb-p3-tv0"/>
|
||||||
<outlet property="_emptyViewLabel" destination="tEp-qe-xvE" id="CPZ-it-kVY"/>
|
<outlet property="_emptyViewLabel" destination="tEp-qe-xvE" id="CPZ-it-kVY"/>
|
||||||
|
<outlet property="_loadingView" destination="1MY-io-Uh6" id="hAN-TF-dIn"/>
|
||||||
|
<outlet property="_searchBar" destination="FnG-4H-B7H" id="UJI-d9-IUd"/>
|
||||||
|
<outlet property="_tableViewTopToSearchBarConstraint" destination="pGe-1e-B4s" id="P32-0j-8YX"/>
|
||||||
<segue destination="1845" kind="presentation" identifier="loginAddSegue" modalPresentationStyle="fullScreen" modalTransitionStyle="coverVertical" id="3731"/>
|
<segue destination="1845" kind="presentation" identifier="loginAddSegue" modalPresentationStyle="fullScreen" modalTransitionStyle="coverVertical" id="3731"/>
|
||||||
<segue destination="11552" kind="show" identifier="loginSearchFromListSegue" id="12574"/>
|
<segue destination="11552" kind="show" identifier="loginSearchFromListSegue" id="12574"/>
|
||||||
</connections>
|
</connections>
|
||||||
@@ -619,7 +643,7 @@
|
|||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<inferredMetricsTieBreakers>
|
<inferredMetricsTieBreakers>
|
||||||
<segue reference="12959"/>
|
<segue reference="12574"/>
|
||||||
<segue reference="3731"/>
|
<segue reference="3731"/>
|
||||||
</inferredMetricsTieBreakers>
|
</inferredMetricsTieBreakers>
|
||||||
<resources>
|
<resources>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Bit.iOS.Autofill.Models
|
|||||||
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
|
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
|
||||||
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
||||||
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
||||||
|
public ASPasskeyCredentialRequestParameters PasskeyCredentialRequestParameters { get; set; }
|
||||||
public bool Configuring { get; set; }
|
public bool Configuring { get; set; }
|
||||||
public bool IsExecutingWithoutUserInteraction { get; set; }
|
public bool IsExecutingWithoutUserInteraction { get; set; }
|
||||||
|
|
||||||
@@ -29,6 +30,12 @@ namespace Bit.iOS.Autofill.Models
|
|||||||
/// Param: isUserVerified if the user was verified. If null then the verification hasn't been done.
|
/// Param: isUserVerified if the user was verified. If null then the verification hasn't been done.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TaskCompletionSource<(string cipherId, bool? isUserVerified)> PickCredentialForFido2CreationTcs { get; set; }
|
public TaskCompletionSource<(string cipherId, bool? isUserVerified)> PickCredentialForFido2CreationTcs { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This is used to defer the completion until a vault item is chosen to use the passkey.
|
||||||
|
/// Param: cipher ID to add the passkey to.
|
||||||
|
/// </summary>
|
||||||
|
public TaskCompletionSource<string> PickCredentialForFido2GetAssertionFromListTcs { get; set; }
|
||||||
|
|
||||||
public bool VaultUnlockedDuringThisSession { get; set; }
|
public bool VaultUnlockedDuringThisSession { get; set; }
|
||||||
|
|
||||||
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
||||||
@@ -62,5 +69,9 @@ namespace Bit.iOS.Autofill.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool IsPasskey => PasskeyCredentialRequest != null;
|
public bool IsPasskey => PasskeyCredentialRequest != null;
|
||||||
|
|
||||||
|
public bool IsPreparingListForPasskey => PasskeyCredentialRequestParameters != null;
|
||||||
|
|
||||||
|
public bool IsCreatingOrPreparingListForPasskey => IsCreatingPasskey || IsPreparingListForPasskey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Bit.App.Abstractions;
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.iOS.Core.Models;
|
||||||
using Bit.iOS.Core.Utilities;
|
using Bit.iOS.Core.Utilities;
|
||||||
using Bit.iOS.Core.Views;
|
using Bit.iOS.Core.Views;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
@@ -13,26 +14,11 @@ namespace Bit.iOS.Autofill.Utilities
|
|||||||
{
|
{
|
||||||
public static class AutofillHelpers
|
public static class AutofillHelpers
|
||||||
{
|
{
|
||||||
public async static Task TableRowSelectedAsync(UITableView tableView, NSIndexPath indexPath,
|
public async static Task TableRowSelectedAsync(CipherViewModel item, ExtensionTableSource tableSource,
|
||||||
ExtensionTableSource tableSource, CredentialProviderViewController cpViewController,
|
CredentialProviderViewController cpViewController,
|
||||||
UIViewController controller, IPasswordRepromptService passwordRepromptService,
|
UIViewController controller,
|
||||||
string loginAddSegue)
|
IPasswordRepromptService passwordRepromptService)
|
||||||
{
|
{
|
||||||
tableView.DeselectRow(indexPath, true);
|
|
||||||
tableView.EndEditing(true);
|
|
||||||
|
|
||||||
if (tableSource.Items == null || tableSource.Items.Count() == 0)
|
|
||||||
{
|
|
||||||
controller.PerformSegue(loginAddSegue, tableSource);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var item = tableSource.Items.ElementAt(indexPath.Row);
|
|
||||||
if (item == null)
|
|
||||||
{
|
|
||||||
cpViewController.CompleteRequest();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(item.Reprompt))
|
if (!await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(item.Reprompt))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.iOS.Autofill.ListItems;
|
||||||
using Bit.iOS.Autofill.Models;
|
using Bit.iOS.Autofill.Models;
|
||||||
|
using Bit.iOS.Core.Controllers;
|
||||||
|
using Bit.iOS.Core.Models;
|
||||||
|
using Bit.iOS.Core.Utilities;
|
||||||
using Bit.iOS.Core.Views;
|
using Bit.iOS.Core.Views;
|
||||||
|
using CoreGraphics;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
@@ -16,9 +24,13 @@ namespace Bit.iOS.Autofill.Utilities
|
|||||||
public abstract class BaseLoginListTableSource<T> : ExtensionTableSource
|
public abstract class BaseLoginListTableSource<T> : ExtensionTableSource
|
||||||
where T : UIViewController, ILoginListViewController
|
where T : UIViewController, ILoginListViewController
|
||||||
{
|
{
|
||||||
private IPasswordRepromptService _passwordRepromptService;
|
private const string NoDataCellIdentifier = "NoDataCellIdentifier";
|
||||||
|
|
||||||
|
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||||
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||||
|
|
||||||
|
List<string> _allowedFido2CipherIds = null;
|
||||||
|
|
||||||
public BaseLoginListTableSource(T controller)
|
public BaseLoginListTableSource(T controller)
|
||||||
: base(controller.Context, controller)
|
: base(controller.Context, controller)
|
||||||
{
|
{
|
||||||
@@ -31,6 +43,209 @@ namespace Bit.iOS.Autofill.Utilities
|
|||||||
|
|
||||||
protected abstract string LoginAddSegue { get; }
|
protected abstract string LoginAddSegue { get; }
|
||||||
|
|
||||||
|
public bool IsEmpty => Items?.Any() != true;
|
||||||
|
|
||||||
|
public override void RegisterTableViewCells(UITableView tableView)
|
||||||
|
{
|
||||||
|
base.RegisterTableViewCells(tableView);
|
||||||
|
|
||||||
|
tableView.RegisterClassForCellReuse(typeof(ExtendedUITableViewCell), NoDataCellIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnItemsLoaded(string searchFilter, CancellationToken ct)
|
||||||
|
{
|
||||||
|
base.OnItemsLoaded(searchFilter, ct);
|
||||||
|
|
||||||
|
if (Context.IsPreparingListForPasskey && _allowedFido2CipherIds != null)
|
||||||
|
{
|
||||||
|
LoadFido2Ciphers(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
Controller.OnItemsLoaded(searchFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFido2Ciphers(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var fido2CiphersToInsert = new List<CipherViewModel>();
|
||||||
|
foreach (var item in Items.Where(i => i?.CipherView?.HasFido2Credential == true))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!_allowedFido2CipherIds.Any()
|
||||||
|
||
|
||||||
|
_allowedFido2CipherIds.Contains(item.Id))
|
||||||
|
{
|
||||||
|
fido2CiphersToInsert.Add(item.ToPasskeyListItemCipherViewModel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fido2CiphersToInsert.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fido2CiphersToInsert.Reverse();
|
||||||
|
|
||||||
|
foreach (var item in fido2CiphersToInsert)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Items.Insert(0, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override CipherViewModel CreateCipherViewModel(CipherView cipherView)
|
||||||
|
{
|
||||||
|
var vm = base.CreateCipherViewModel(cipherView);
|
||||||
|
vm.ForceSectionIcon = Context.IsPreparingListForPasskey;
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ShouldUseMainIconAsPasskey(CipherViewModel item, NSIndexPath indexPath)
|
||||||
|
{
|
||||||
|
if (!item.HasFido2Credential)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsPasskeySection(indexPath.Section) || !item.ForceSectionIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override UIView GetViewForHeader(UITableView tableView, nint section)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Context.IsCreatingOrPreparingListForPasskey
|
||||||
|
&&
|
||||||
|
tableView.DequeueReusableHeaderFooterView(LoginListViewController.HEADER_SECTION_IDENTIFIER) is HeaderItemView headerItemView)
|
||||||
|
{
|
||||||
|
if (Context.IsCreatingPasskey)
|
||||||
|
{
|
||||||
|
headerItemView.SetHeaderText(AppResources.ChooseALoginToSaveThisPasskeyTo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headerItemView.SetHeaderText(IsPasskeySection(section) ? AppResources.Passkeys : AppResources.Passwords);
|
||||||
|
}
|
||||||
|
return headerItemView;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UIView(CGRect.Empty);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
return new UIView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override nint NumberOfSections(UITableView tableView)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Context.IsPreparingListForPasskey)
|
||||||
|
{
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override nint RowsInSection(UITableView tableview, nint section)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Context.IsCreatingPasskey)
|
||||||
|
{
|
||||||
|
return Items?.Count() ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Context.IsPreparingListForPasskey)
|
||||||
|
{
|
||||||
|
var isPasskeySection = IsPasskeySection(section);
|
||||||
|
var count = GetNumberOfItems(isPasskeySection);
|
||||||
|
|
||||||
|
return count == 0 ? 1 : count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.RowsInSection(tableview, section);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetNumberOfItems(bool forFido2)
|
||||||
|
{
|
||||||
|
if (!Context.IsPreparingListForPasskey)
|
||||||
|
{
|
||||||
|
return Items?.Count() ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Items?.Count(i => i.IsFido2ListItem == forFido2) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (GetNumberOfItems(IsPasskeySection(indexPath.Section)) == 0)
|
||||||
|
{
|
||||||
|
var noDataCell = tableView.DequeueReusableCell(NoDataCellIdentifier);
|
||||||
|
|
||||||
|
var text = AppResources.NoItemsToList;
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(14, 0))
|
||||||
|
{
|
||||||
|
var config = noDataCell.DefaultContentConfiguration;
|
||||||
|
config.Text = text;
|
||||||
|
config.TextProperties.Color = ThemeHelpers.TextColor;
|
||||||
|
config.TextProperties.Alignment = UIListContentTextAlignment.Center;
|
||||||
|
config.TextProperties.LineBreakMode = UILineBreakMode.WordWrap;
|
||||||
|
config.TextProperties.NumberOfLines = 0;
|
||||||
|
noDataCell.ContentConfiguration = config;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
noDataCell.TextLabel.Text = text;
|
||||||
|
noDataCell.TextLabel.TextAlignment = UITextAlignment.Center;
|
||||||
|
noDataCell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap;
|
||||||
|
noDataCell.TextLabel.Lines = 0;
|
||||||
|
noDataCell.TextLabel.TextColor = ThemeHelpers.TextColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return noDataCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cell = tableView.DequeueReusableCell(CipherLoginCellIdentifier);
|
||||||
|
if (cell is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"The cell {CipherLoginCellIdentifier} has not been registered in the UITableView");
|
||||||
|
}
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
return new ExtendedUITableViewCell(UITableViewCellStyle.Default, "NoDataCell");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReloadWithAllowedFido2Credentials(List<string> allowedCipherIds)
|
||||||
|
{
|
||||||
|
_allowedFido2CipherIds = allowedCipherIds;
|
||||||
|
Controller.ReloadItemsAsync().FireAndForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPasskeySection(nint section) => section == 0;
|
||||||
|
|
||||||
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -41,8 +256,26 @@ namespace Bit.iOS.Autofill.Utilities
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
if (Items == null || Items.Count() == 0)
|
||||||
Controller.CPViewController, Controller, _passwordRepromptService, LoginAddSegue);
|
{
|
||||||
|
Controller.PerformSegue(LoginAddSegue, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = await DeselectRowAndGetItemAsync(tableView, indexPath);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Context.IsPreparingListForPasskey && item.IsFido2ListItem)
|
||||||
|
{
|
||||||
|
Context.PickCredentialForFido2GetAssertionFromListTcs.SetResult(item.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AutofillHelpers.TableRowSelectedAsync(item, this,
|
||||||
|
Controller.CPViewController, Controller, _passwordRepromptService);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -52,13 +285,9 @@ namespace Bit.iOS.Autofill.Utilities
|
|||||||
|
|
||||||
private async Task SelectRowForPasskeyCreationAsync(UITableView tableView, NSIndexPath indexPath)
|
private async Task SelectRowForPasskeyCreationAsync(UITableView tableView, NSIndexPath indexPath)
|
||||||
{
|
{
|
||||||
tableView.DeselectRow(indexPath, true);
|
var item = await DeselectRowAndGetItemAsync(tableView, indexPath);
|
||||||
tableView.EndEditing(true);
|
|
||||||
|
|
||||||
var item = Items.ElementAt(indexPath.Row);
|
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
await _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +309,31 @@ namespace Bit.iOS.Autofill.Utilities
|
|||||||
|
|
||||||
Context.PickCredentialForFido2CreationTcs.SetResult((item.Id, null));
|
Context.PickCredentialForFido2CreationTcs.SetResult((item.Id, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<CipherViewModel> DeselectRowAndGetItemAsync(UITableView tableView, NSIndexPath indexPath)
|
||||||
|
{
|
||||||
|
tableView.DeselectRow(indexPath, true);
|
||||||
|
tableView.EndEditing(true);
|
||||||
|
|
||||||
|
var item = Items.ElementAtOrDefault(GetIndexForItemAt(tableView, indexPath));
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
await _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetIndexForItemAt(UITableView tableView, NSIndexPath indexPath)
|
||||||
|
{
|
||||||
|
var index = indexPath.Row;
|
||||||
|
if (indexPath.Section == 1)
|
||||||
|
{
|
||||||
|
index += (int)RowsInSection(tableView, 0);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
using Bit.iOS.Autofill.Models;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Autofill.Utilities
|
||||||
|
{
|
||||||
|
public class Fido2GetAssertionFromListUserInterface : IFido2GetAssertionUserInterface
|
||||||
|
{
|
||||||
|
private readonly Context _context;
|
||||||
|
private readonly Func<Task> _ensureUnlockedVaultCallback;
|
||||||
|
private readonly Func<bool> _hasVaultBeenUnlockedInThisTransaction;
|
||||||
|
private readonly Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback;
|
||||||
|
private readonly Action<List<string>> _onAllowedFido2Credentials;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation to perform the interactions with the UI directly from a list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Current context</param>
|
||||||
|
/// <param name="ensureUnlockedVaultCallback">Call to ensure the vault is unlocekd</param>
|
||||||
|
/// <param name="hasVaultBeenUnlockedInThisTransaction">Check if vault has been unlocked in this transaction</param>
|
||||||
|
/// <param name="verifyUserCallback">Call to perform user verification to a given cipherId and preference</param>
|
||||||
|
/// <param name="onAllowedFido2Credentials">Action to be performed on allowed Fido2 credentials, each one is a cipherId</param>
|
||||||
|
public Fido2GetAssertionFromListUserInterface(Context context,
|
||||||
|
Func<Task> ensureUnlockedVaultCallback,
|
||||||
|
Func<bool> hasVaultBeenUnlockedInThisTransaction,
|
||||||
|
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback,
|
||||||
|
Action<List<string>> onAllowedFido2Credentials)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||||
|
_hasVaultBeenUnlockedInThisTransaction = hasVaultBeenUnlockedInThisTransaction;
|
||||||
|
_verifyUserCallback = verifyUserCallback;
|
||||||
|
_onAllowedFido2Credentials = onAllowedFido2Credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasVaultBeenUnlockedInThisTransaction { get; private set; }
|
||||||
|
|
||||||
|
public async Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
|
||||||
|
{
|
||||||
|
if (credentials is null || credentials.Length == 0)
|
||||||
|
{
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
HasVaultBeenUnlockedInThisTransaction = _hasVaultBeenUnlockedInThisTransaction();
|
||||||
|
|
||||||
|
_onAllowedFido2Credentials?.Invoke(credentials.Select(c => c.CipherId).ToList() ?? new List<string>());
|
||||||
|
|
||||||
|
_context.PickCredentialForFido2GetAssertionFromListTcs?.SetCanceled();
|
||||||
|
_context.PickCredentialForFido2GetAssertionFromListTcs = new TaskCompletionSource<string>();
|
||||||
|
|
||||||
|
var cipherId = await _context.PickCredentialForFido2GetAssertionFromListTcs.Task;
|
||||||
|
|
||||||
|
var credential = credentials.First(c => c.CipherId == cipherId);
|
||||||
|
|
||||||
|
var verified = await _verifyUserCallback(cipherId, credential.UserVerificationPreference);
|
||||||
|
|
||||||
|
return (CipherId: cipherId, UserVerified: verified);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureUnlockedVaultAsync()
|
||||||
|
{
|
||||||
|
await _ensureUnlockedVaultCallback();
|
||||||
|
|
||||||
|
HasVaultBeenUnlockedInThisTransaction = _hasVaultBeenUnlockedInThisTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -93,6 +93,7 @@
|
|||||||
<Compile Include="ILoginListViewController.cs" />
|
<Compile Include="ILoginListViewController.cs" />
|
||||||
<Compile Include="Fido2MakeCredentialUserInterface.cs" />
|
<Compile Include="Fido2MakeCredentialUserInterface.cs" />
|
||||||
<Compile Include="Utilities\InvalidOperationNeedsUIException.cs" />
|
<Compile Include="Utilities\InvalidOperationNeedsUIException.cs" />
|
||||||
|
<Compile Include="Utilities\Fido2GetAssertionFromListUserInterface.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<BundleResource Include="Resources\check.png" />
|
<BundleResource Include="Resources\check.png" />
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Bit.iOS.Core.Models
|
namespace Bit.iOS.Core.Models
|
||||||
{
|
{
|
||||||
@@ -30,6 +27,8 @@ namespace Bit.iOS.Core.Models
|
|||||||
public List<Tuple<string, string>> Fields { get; set; }
|
public List<Tuple<string, string>> Fields { get; set; }
|
||||||
public CipherView CipherView { get; set; }
|
public CipherView CipherView { get; set; }
|
||||||
public CipherRepromptType Reprompt { get; set; }
|
public CipherRepromptType Reprompt { get; set; }
|
||||||
|
public bool IsFido2ListItem { get; set; }
|
||||||
|
public bool ForceSectionIcon { get; set; }
|
||||||
|
|
||||||
public bool HasFido2Credential => CipherView?.HasFido2Credential ?? false;
|
public bool HasFido2Credential => CipherView?.HasFido2Credential ?? false;
|
||||||
|
|
||||||
@@ -46,5 +45,13 @@ namespace Bit.iOS.Core.Models
|
|||||||
public string Uri { get; set; }
|
public string Uri { get; set; }
|
||||||
public UriMatchType? Match { get; set; }
|
public UriMatchType? Match { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CipherViewModel ToPasskeyListItemCipherViewModel()
|
||||||
|
{
|
||||||
|
var vm = new CipherViewModel(CipherView);
|
||||||
|
vm.IsFido2ListItem = true;
|
||||||
|
vm.ForceSectionIcon = ForceSectionIcon;
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace Bit.iOS.Core.Views
|
|||||||
|
|
||||||
_mainIcon.Font = UIFont.FromName("bwi-font", 24);
|
_mainIcon.Font = UIFont.FromName("bwi-font", 24);
|
||||||
_mainIcon.AdjustsFontSizeToFitWidth = true;
|
_mainIcon.AdjustsFontSizeToFitWidth = true;
|
||||||
|
_mainIcon.Text = BitwardenIcons.Globe;
|
||||||
_mainIcon.TextColor = ThemeHelpers.PrimaryColor;
|
_mainIcon.TextColor = ThemeHelpers.PrimaryColor;
|
||||||
|
|
||||||
_orgIcon.Font = UIFont.FromName("bwi-font", 15);
|
_orgIcon.Font = UIFont.FromName("bwi-font", 15);
|
||||||
@@ -103,7 +104,7 @@ namespace Bit.iOS.Core.Views
|
|||||||
|
|
||||||
public void SetSubtitle(string subtitle) => _subtitle.Text = subtitle;
|
public void SetSubtitle(string subtitle) => _subtitle.Text = subtitle;
|
||||||
|
|
||||||
public void SetHasFido2Credential(bool has) => _mainIcon.Text = has ? BitwardenIcons.Passkey : BitwardenIcons.Globe;
|
public void UpdateMainIcon(bool usePasskeyIcon) => _mainIcon.Text = usePasskeyIcon ? BitwardenIcons.Passkey : BitwardenIcons.Globe;
|
||||||
|
|
||||||
public void ShowOrganizationIcon() => _orgIcon.Hidden = false;
|
public void ShowOrganizationIcon() => _orgIcon.Hidden = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.iOS.Core.Controllers;
|
using Bit.iOS.Core.Controllers;
|
||||||
using Bit.iOS.Core.Models;
|
using Bit.iOS.Core.Models;
|
||||||
@@ -12,9 +13,9 @@ namespace Bit.iOS.Core.Views
|
|||||||
{
|
{
|
||||||
public class ExtensionTableSource : ExtendedUITableViewSource
|
public class ExtensionTableSource : ExtendedUITableViewSource
|
||||||
{
|
{
|
||||||
public const string CellIdentifier = nameof(CipherLoginTableViewCell);
|
public const string CipherLoginCellIdentifier = nameof(CipherLoginTableViewCell);
|
||||||
|
|
||||||
private IEnumerable<CipherViewModel> _allItems = new List<CipherViewModel>();
|
protected IEnumerable<CipherViewModel> _allItems = new List<CipherViewModel>();
|
||||||
protected ICipherService _cipherService;
|
protected ICipherService _cipherService;
|
||||||
protected ITotpService _totpService;
|
protected ITotpService _totpService;
|
||||||
protected IStateService _stateService;
|
protected IStateService _stateService;
|
||||||
@@ -34,7 +35,7 @@ namespace Bit.iOS.Core.Views
|
|||||||
Items = new List<CipherViewModel>();
|
Items = new List<CipherViewModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<CipherViewModel> Items { get; private set; }
|
public IList<CipherViewModel> Items { get; private set; }
|
||||||
|
|
||||||
public virtual async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
|
public virtual async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
|
||||||
{
|
{
|
||||||
@@ -66,11 +67,11 @@ namespace Bit.iOS.Core.Views
|
|||||||
|
|
||||||
return combinedLogins
|
return combinedLogins
|
||||||
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
|
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
|
||||||
.Select(s => new CipherViewModel(s))
|
.Select(CreateCipherViewModel)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FilterResults(string searchFilter, CancellationToken ct)
|
public virtual void FilterResults(string searchFilter, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -84,18 +85,24 @@ namespace Bit.iOS.Core.Views
|
|||||||
var results = _searchService.SearchCiphersAsync(searchFilter,
|
var results = _searchService.SearchCiphersAsync(searchFilter,
|
||||||
c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted, null, ct)
|
c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted, null, ct)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
Items = results.Select(s => new CipherViewModel(s)).ToArray();
|
Items = results.Select(CreateCipherViewModel).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OnItemsLoaded(searchFilter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void OnItemsLoaded(string searchFilter, CancellationToken ct) { }
|
||||||
|
|
||||||
|
protected virtual CipherViewModel CreateCipherViewModel(CipherView cipherView) => new CipherViewModel(cipherView);
|
||||||
|
|
||||||
public override nint RowsInSection(UITableView tableview, nint section)
|
public override nint RowsInSection(UITableView tableview, nint section)
|
||||||
{
|
{
|
||||||
return Items == null || Items.Count() == 0 ? 1 : Items.Count();
|
return Items == null || Items.Count() == 0 ? 1 : Items.Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterTableViewCells(UITableView tableView)
|
public virtual void RegisterTableViewCells(UITableView tableView)
|
||||||
{
|
{
|
||||||
tableView.RegisterClassForCellReuse(typeof(CipherLoginTableViewCell), CellIdentifier);
|
tableView.RegisterClassForCellReuse(typeof(CipherLoginTableViewCell), CipherLoginCellIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
||||||
@@ -111,46 +118,51 @@ namespace Bit.iOS.Core.Views
|
|||||||
return noDataCell;
|
return noDataCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cell = tableView.DequeueReusableCell(CellIdentifier);
|
var cell = tableView.DequeueReusableCell(CipherLoginCellIdentifier);
|
||||||
if (cell is null)
|
if (cell is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"The cell {CellIdentifier} has not been registered in the UITableView");
|
throw new InvalidOperationException($"The cell {CipherLoginCellIdentifier} has not been registered in the UITableView");
|
||||||
}
|
}
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
|
public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
|
||||||
{
|
{
|
||||||
if (Items == null
|
try
|
||||||
|| !Items.Any()
|
|
||||||
|| !(cell is CipherLoginTableViewCell cipherCell))
|
|
||||||
{
|
{
|
||||||
return;
|
if (Items == null
|
||||||
}
|
|| !Items.Any()
|
||||||
|
|| !(cell is CipherLoginTableViewCell cipherCell))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var item = Items.ElementAt(indexPath.Row);
|
var item = Items.ElementAtOrDefault(GetIndexForItemAt(tableView, indexPath));
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cipherCell.SetTitle(item.Name);
|
cipherCell.SetTitle(item.Name);
|
||||||
cipherCell.SetSubtitle(item.Username);
|
cipherCell.SetSubtitle(item.Username);
|
||||||
cipherCell.SetHasFido2Credential(item.HasFido2Credential);
|
cipherCell.UpdateMainIcon(ShouldUseMainIconAsPasskey(item, indexPath));
|
||||||
if (item.IsShared)
|
if (item.IsShared)
|
||||||
|
{
|
||||||
|
cipherCell.ShowOrganizationIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
cipherCell.ShowOrganizationIcon();
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual int GetIndexForItemAt(UITableView tableView, NSIndexPath indexPath) => indexPath.Row;
|
||||||
|
|
||||||
|
protected virtual bool ShouldUseMainIconAsPasskey(CipherViewModel item, NSIndexPath indexPath) => item.HasFido2Credential;
|
||||||
|
|
||||||
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
|
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
|
||||||
{
|
{
|
||||||
if (Items == null
|
|
||||||
|| !Items.Any())
|
|
||||||
{
|
|
||||||
return base.GetHeightForRow(tableView, indexPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 55;
|
return 55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user