diff --git a/src/Core/Resources/Localization/AppResources.Designer.cs b/src/Core/Resources/Localization/AppResources.Designer.cs
index 768419883..6ea8bcc87 100644
--- a/src/Core/Resources/Localization/AppResources.Designer.cs
+++ b/src/Core/Resources/Localization/AppResources.Designer.cs
@@ -5407,6 +5407,15 @@ namespace Bit.Core.Resources.Localization {
}
}
+ ///
+ /// Looks up a localized string similar to Passwords.
+ ///
+ public static string Passwords {
+ get {
+ return ResourceManager.GetString("Passwords", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use..
///
diff --git a/src/Core/Resources/Localization/AppResources.resx b/src/Core/Resources/Localization/AppResources.resx
index c29ae6dba..bfe9be087 100644
--- a/src/Core/Resources/Localization/AppResources.resx
+++ b/src/Core/Resources/Localization/AppResources.resx
@@ -2936,4 +2936,7 @@ Do you want to switch to this account?
There was a problem reading your passkey for {0}. Try again later.
The parameter is the RpId
+
+ Passwords
+
diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs
index 0649f327a..3992911cc 100644
--- a/src/Core/Services/Fido2AuthenticatorService.cs
+++ b/src/Core/Services/Fido2AuthenticatorService.cs
@@ -109,8 +109,9 @@ namespace Bit.Core.Services
{
throw;
}
- catch (Exception)
+ catch (Exception ex)
{
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
throw new UnknownError();
}
}
@@ -203,8 +204,9 @@ namespace Bit.Core.Services
Signature = signature
};
}
- catch (Exception)
+ catch (Exception ex)
{
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
throw new UnknownError();
}
}
diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
index d65816ad4..d5e5b02df 100644
--- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
+++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
@@ -24,7 +24,25 @@ namespace Bit.iOS.Autofill
private readonly LazyResolve _platformUtilsService = new LazyResolve();
private readonly LazyResolve _userVerificationMediatorService = new LazyResolve();
private readonly LazyResolve _cipherService = new LazyResolve();
-
+
+ [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)
{
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
@@ -239,7 +257,7 @@ namespace Bit.iOS.Autofill
}
}
- private async Task VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference)
+ internal async Task VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference)
{
try
{
diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs
index 2cb8bba02..f2cf4d0eb 100644
--- a/src/iOS.Autofill/CredentialProviderViewController.cs
+++ b/src/iOS.Autofill/CredentialProviderViewController.cs
@@ -88,56 +88,13 @@ namespace Bit.iOS.Autofill
}
else
{
- if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
- {
- PerformSegue(SegueConstants.LOGIN_SEARCH, this);
- }
- else
+ if (_context.IsCreatingOrPreparingListForPasskey || _context.ServiceIdentifiers?.Length > 0)
{
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
{
- PerformSegue(SegueConstants.LOGIN_LIST, this);
+ PerformSegue(SegueConstants.LOGIN_SEARCH, this);
}
}
}
@@ -306,6 +263,8 @@ namespace Bit.iOS.Autofill
return;
}
+ _context.PickCredentialForFido2GetAssertionFromListTcs?.TrySetCanceled();
+
if (!string.IsNullOrWhiteSpace(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);
CancelRequest(ASExtensionErrorCode.Failed);
diff --git a/src/iOS.Autofill/ILoginListViewController.cs b/src/iOS.Autofill/ILoginListViewController.cs
index 8f515d3f3..93fccc7ac 100644
--- a/src/iOS.Autofill/ILoginListViewController.cs
+++ b/src/iOS.Autofill/ILoginListViewController.cs
@@ -1,4 +1,5 @@
-using Bit.iOS.Autofill.Models;
+using System.Threading.Tasks;
+using Bit.iOS.Autofill.Models;
namespace Bit.iOS.Autofill
{
@@ -6,5 +7,8 @@ namespace Bit.iOS.Autofill
{
Context Context { get; }
CredentialProviderViewController CPViewController { get; }
+ void OnItemsLoaded(string searchFilter);
+ Task ReloadItemsAsync();
+ void ReloadTableViewData();
}
}
diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs
index 7c1836f8e..fbda1d7fd 100644
--- a/src/iOS.Autofill/LoginListViewController.cs
+++ b/src/iOS.Autofill/LoginListViewController.cs
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
+using AuthenticationServices;
using Bit.App.Controls;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
@@ -15,8 +16,8 @@ using Bit.iOS.Core.Controllers;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views;
using CoreFoundation;
-using CoreGraphics;
using Foundation;
+using Microsoft.Maui.ApplicationModel;
using UIKit;
namespace Bit.iOS.Autofill
@@ -45,8 +46,30 @@ namespace Bit.iOS.Autofill
LazyResolve _platformUtilsService = new LazyResolve();
LazyResolve _logger = new LazyResolve();
LazyResolve _userVerificationMediatorService = new LazyResolve();
+ LazyResolve _fido2AuthenticatorService = new LazyResolve();
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()
{
@@ -58,16 +81,22 @@ namespace Bit.iOS.Autofill
SubscribeSyncCompleted();
- NavItem.Title = Context.IsCreatingPasskey ? AppResources.SavePasskey : AppResources.Items;
+ NavItem.Title = NavTitle;
+
_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;
var tableSource = new TableSource(this);
TableView.Source = tableSource;
tableSource.RegisterTableViewCells(TableView);
- if (Context.IsCreatingPasskey)
+ if (Context.IsCreatingOrPreparingListForPasskey)
{
TableView.SectionHeaderHeight = 55;
TableView.RegisterClassForHeaderFooterViewReuse(typeof(HeaderItemView), HEADER_SECTION_IDENTIFIER);
@@ -78,8 +107,6 @@ namespace Bit.iOS.Autofill
TableView.SectionHeaderTopPadding = 0;
}
- await ((TableSource)TableView.Source).LoadAsync();
-
if (Context.IsCreatingPasskey)
{
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, Context.UrlString);
@@ -91,8 +118,6 @@ namespace Bit.iOS.Autofill
_emptyViewButton.ClipsToBounds = true;
}
- _alreadyLoadItemsOnce = true;
-
var storageService = ServiceContainer.Resolve("storageService");
var needsAutofillReplacement = await storageService.GetAsync(
Core.Constants.AutofillNeedsIdentityReplacementKey);
@@ -113,6 +138,22 @@ namespace Bit.iOS.Autofill
}, false);
_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)
{
@@ -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)
{
Cancel();
@@ -142,7 +251,54 @@ namespace Bit.iOS.Autofill
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)
@@ -250,8 +406,7 @@ namespace Bit.iOS.Autofill
{
try
{
- await ((TableSource)TableView.Source).LoadAsync();
- TableView.ReloadData();
+ await ReloadItemsAsync();
}
catch (Exception ex)
{
@@ -262,10 +417,18 @@ namespace Bit.iOS.Autofill
});
}
- public void OnEmptyList()
+ public void OnItemsLoaded(string searchFilter)
{
- _emptyView.Hidden = false;
- TableView.Hidden = true;
+ if (Context.IsCreatingPasskey)
+ {
+ _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()
@@ -281,8 +444,7 @@ namespace Bit.iOS.Autofill
{
try
{
- await ((TableSource)TableView.Source).LoadAsync();
- TableView.ReloadData();
+ await ReloadItemsAsync();
}
catch (Exception ex)
{
@@ -306,6 +468,53 @@ namespace Bit.iOS.Autofill
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
{
public TableSource(LoginListViewController controller)
@@ -314,54 +523,6 @@ namespace Bit.iOS.Autofill
}
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);
- }
}
}
}
diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs
index 82c06d363..fa34b2413 100644
--- a/src/iOS.Autofill/LoginListViewController.designer.cs
+++ b/src/iOS.Autofill/LoginListViewController.designer.cs
@@ -24,6 +24,15 @@ namespace Bit.iOS.Autofill
[Outlet]
UIKit.UILabel _emptyViewLabel { get; set; }
+ [Outlet]
+ UIKit.UIActivityIndicatorView _loadingView { get; set; }
+
+ [Outlet]
+ UIKit.UISearchBar _searchBar { get; set; }
+
+ [Outlet]
+ UIKit.NSLayoutConstraint _tableViewTopToSearchBarConstraint { get; set; }
+
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIBarButtonItem AddBarButton { get; set; }
@@ -72,6 +81,16 @@ namespace Bit.iOS.Autofill
_emptyViewLabel = null;
}
+ if (_searchBar != null) {
+ _searchBar.Dispose ();
+ _searchBar = null;
+ }
+
+ if (_tableViewTopToSearchBarConstraint != null) {
+ _tableViewTopToSearchBarConstraint.Dispose ();
+ _tableViewTopToSearchBarConstraint = null;
+ }
+
if (AddBarButton != null) {
AddBarButton.Dispose ();
AddBarButton = null;
@@ -96,6 +115,11 @@ namespace Bit.iOS.Autofill
TableView.Dispose ();
TableView = null;
}
+
+ if (_loadingView != null) {
+ _loadingView.Dispose ();
+ _loadingView = null;
+ }
}
}
}
diff --git a/src/iOS.Autofill/LoginSearchViewController.cs b/src/iOS.Autofill/LoginSearchViewController.cs
index cc7380cd3..c691e2443 100644
--- a/src/iOS.Autofill/LoginSearchViewController.cs
+++ b/src/iOS.Autofill/LoginSearchViewController.cs
@@ -1,14 +1,14 @@
using System;
+using System.Threading.Tasks;
+using Bit.Core.Resources.Localization;
+using Bit.Core.Services;
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 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
{
@@ -26,22 +26,35 @@ namespace Bit.iOS.Autofill
public async override void ViewDidLoad()
{
- base.ViewDidLoad();
- NavItem.Title = AppResources.SearchVault;
- CancelBarButton.Title = AppResources.Cancel;
- SearchBar.Placeholder = AppResources.Search;
- SearchBar.BackgroundColor = SearchBar.BarTintColor = ThemeHelpers.ListHeaderBackgroundColor;
- SearchBar.UpdateThemeIfNeeded();
+ try
+ {
+ base.ViewDidLoad();
- TableView.RowHeight = UITableView.AutomaticDimension;
- TableView.EstimatedRowHeight = 44;
-
- var tableSource = new TableSource(this);
- TableView.Source = tableSource;
- tableSource.RegisterTableViewCells(TableView);
-
- SearchBar.Delegate = new ExtensionSearchDelegate(TableView);
- await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
+ NavItem.Title = AppResources.SearchVault;
+ CancelBarButton.Title = AppResources.Cancel;
+ SearchBar.Placeholder = AppResources.Search;
+ SearchBar.BackgroundColor = SearchBar.BarTintColor = ThemeHelpers.ListHeaderBackgroundColor;
+ SearchBar.UpdateThemeIfNeeded();
+
+ TableView.RowHeight = UITableView.AutomaticDimension;
+ TableView.EstimatedRowHeight = 55;
+
+ 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)
@@ -90,11 +103,20 @@ namespace Bit.iOS.Autofill
{
DismissViewController(true, async () =>
{
- await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
- TableView.ReloadData();
+ await ReloadItemsAsync();
});
}
+ 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
{
public TableSource(LoginSearchViewController controller)
diff --git a/src/iOS.Autofill/MainInterface.storyboard b/src/iOS.Autofill/MainInterface.storyboard
index bc59c7347..f54e1fde9 100644
--- a/src/iOS.Autofill/MainInterface.storyboard
+++ b/src/iOS.Autofill/MainInterface.storyboard
@@ -132,6 +132,13 @@
+
+
+
+
+
+
+
@@ -172,7 +179,10 @@
-
+
+
+
+
@@ -219,14 +229,25 @@
+
+
+
+
-
+
+
+
+
+
+
+
+
@@ -256,6 +277,9 @@
+
+
+
@@ -619,7 +643,7 @@
-
+
diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs
index ecd16a0da..a1bf25de7 100644
--- a/src/iOS.Autofill/Models/Context.cs
+++ b/src/iOS.Autofill/Models/Context.cs
@@ -14,6 +14,7 @@ namespace Bit.iOS.Autofill.Models
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
+ public ASPasskeyCredentialRequestParameters PasskeyCredentialRequestParameters { get; set; }
public bool Configuring { 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.
///
public TaskCompletionSource<(string cipherId, bool? isUserVerified)> PickCredentialForFido2CreationTcs { get; set; }
+ ///
+ /// 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.
+ ///
+ public TaskCompletionSource PickCredentialForFido2GetAssertionFromListTcs { get; set; }
+
public bool VaultUnlockedDuringThisSession { get; set; }
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
@@ -62,5 +69,9 @@ namespace Bit.iOS.Autofill.Models
}
public bool IsPasskey => PasskeyCredentialRequest != null;
+
+ public bool IsPreparingListForPasskey => PasskeyCredentialRequestParameters != null;
+
+ public bool IsCreatingOrPreparingListForPasskey => IsCreatingPasskey || IsPreparingListForPasskey;
}
}
diff --git a/src/iOS.Autofill/Utilities/AutofillHelpers.cs b/src/iOS.Autofill/Utilities/AutofillHelpers.cs
index b0fb9acb0..c5613fa94 100644
--- a/src/iOS.Autofill/Utilities/AutofillHelpers.cs
+++ b/src/iOS.Autofill/Utilities/AutofillHelpers.cs
@@ -4,6 +4,7 @@ using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
+using Bit.iOS.Core.Models;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views;
using Foundation;
@@ -13,26 +14,11 @@ namespace Bit.iOS.Autofill.Utilities
{
public static class AutofillHelpers
{
- public async static Task TableRowSelectedAsync(UITableView tableView, NSIndexPath indexPath,
- ExtensionTableSource tableSource, CredentialProviderViewController cpViewController,
- UIViewController controller, IPasswordRepromptService passwordRepromptService,
- string loginAddSegue)
+ public async static Task TableRowSelectedAsync(CipherViewModel item, ExtensionTableSource tableSource,
+ CredentialProviderViewController cpViewController,
+ UIViewController controller,
+ 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))
{
return;
diff --git a/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs b/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
index e792eca97..2d86386ff 100644
--- a/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
+++ b/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
@@ -1,13 +1,21 @@
using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
+using Bit.Core.Models.View;
using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities;
+using Bit.iOS.Autofill.ListItems;
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 CoreGraphics;
using Foundation;
using UIKit;
@@ -16,9 +24,13 @@ namespace Bit.iOS.Autofill.Utilities
public abstract class BaseLoginListTableSource : ExtensionTableSource
where T : UIViewController, ILoginListViewController
{
- private IPasswordRepromptService _passwordRepromptService;
+ private const string NoDataCellIdentifier = "NoDataCellIdentifier";
+
+ private readonly IPasswordRepromptService _passwordRepromptService;
private readonly LazyResolve _platformUtilsService = new LazyResolve();
+ List _allowedFido2CipherIds = null;
+
public BaseLoginListTableSource(T controller)
: base(controller.Context, controller)
{
@@ -31,6 +43,209 @@ namespace Bit.iOS.Autofill.Utilities
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();
+ 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 allowedCipherIds)
+ {
+ _allowedFido2CipherIds = allowedCipherIds;
+ Controller.ReloadItemsAsync().FireAndForget();
+ }
+
+ bool IsPasskeySection(nint section) => section == 0;
+
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
try
@@ -41,8 +256,26 @@ namespace Bit.iOS.Autofill.Utilities
return;
}
- await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
- Controller.CPViewController, Controller, _passwordRepromptService, LoginAddSegue);
+ if (Items == null || Items.Count() == 0)
+ {
+ 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)
{
@@ -52,13 +285,9 @@ namespace Bit.iOS.Autofill.Utilities
private async Task SelectRowForPasskeyCreationAsync(UITableView tableView, NSIndexPath indexPath)
{
- tableView.DeselectRow(indexPath, true);
- tableView.EndEditing(true);
-
- var item = Items.ElementAt(indexPath.Row);
+ var item = await DeselectRowAndGetItemAsync(tableView, indexPath);
if (item is null)
{
- await _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
return;
}
@@ -80,6 +309,31 @@ namespace Bit.iOS.Autofill.Utilities
Context.PickCredentialForFido2CreationTcs.SetResult((item.Id, null));
}
+
+ private async Task 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;
+ }
}
}
diff --git a/src/iOS.Autofill/Utilities/Fido2GetAssertionFromListUserInterface.cs b/src/iOS.Autofill/Utilities/Fido2GetAssertionFromListUserInterface.cs
new file mode 100644
index 000000000..d5dca7ee5
--- /dev/null
+++ b/src/iOS.Autofill/Utilities/Fido2GetAssertionFromListUserInterface.cs
@@ -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 _ensureUnlockedVaultCallback;
+ private readonly Func _hasVaultBeenUnlockedInThisTransaction;
+ private readonly Func> _verifyUserCallback;
+ private readonly Action> _onAllowedFido2Credentials;
+
+ ///
+ /// Implementation to perform the interactions with the UI directly from a list.
+ ///
+ /// Current context
+ /// Call to ensure the vault is unlocekd
+ /// Check if vault has been unlocked in this transaction
+ /// Call to perform user verification to a given cipherId and preference
+ /// Action to be performed on allowed Fido2 credentials, each one is a cipherId
+ public Fido2GetAssertionFromListUserInterface(Context context,
+ Func ensureUnlockedVaultCallback,
+ Func hasVaultBeenUnlockedInThisTransaction,
+ Func> verifyUserCallback,
+ Action> 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());
+
+ _context.PickCredentialForFido2GetAssertionFromListTcs?.SetCanceled();
+ _context.PickCredentialForFido2GetAssertionFromListTcs = new TaskCompletionSource();
+
+ 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();
+ }
+ }
+}
+
diff --git a/src/iOS.Autofill/iOS.Autofill.csproj b/src/iOS.Autofill/iOS.Autofill.csproj
index 2647b7963..a3ab038cf 100644
--- a/src/iOS.Autofill/iOS.Autofill.csproj
+++ b/src/iOS.Autofill/iOS.Autofill.csproj
@@ -93,6 +93,7 @@
+
diff --git a/src/iOS.Core/Models/CipherViewModel.cs b/src/iOS.Core/Models/CipherViewModel.cs
index e2f19a8e5..146653d06 100644
--- a/src/iOS.Core/Models/CipherViewModel.cs
+++ b/src/iOS.Core/Models/CipherViewModel.cs
@@ -1,8 +1,5 @@
using Bit.Core.Enums;
using Bit.Core.Models.View;
-using System;
-using System.Collections.Generic;
-using System.Linq;
namespace Bit.iOS.Core.Models
{
@@ -30,6 +27,8 @@ namespace Bit.iOS.Core.Models
public List> Fields { get; set; }
public CipherView CipherView { get; set; }
public CipherRepromptType Reprompt { get; set; }
+ public bool IsFido2ListItem { get; set; }
+ public bool ForceSectionIcon { get; set; }
public bool HasFido2Credential => CipherView?.HasFido2Credential ?? false;
@@ -46,5 +45,13 @@ namespace Bit.iOS.Core.Models
public string Uri { get; set; }
public UriMatchType? Match { get; set; }
}
+
+ public CipherViewModel ToPasskeyListItemCipherViewModel()
+ {
+ var vm = new CipherViewModel(CipherView);
+ vm.IsFido2ListItem = true;
+ vm.ForceSectionIcon = ForceSectionIcon;
+ return vm;
+ }
}
}
diff --git a/src/iOS.Core/Views/CipherLoginTableViewCell.cs b/src/iOS.Core/Views/CipherLoginTableViewCell.cs
index 55b01b165..c0c0fd6e9 100644
--- a/src/iOS.Core/Views/CipherLoginTableViewCell.cs
+++ b/src/iOS.Core/Views/CipherLoginTableViewCell.cs
@@ -47,6 +47,7 @@ namespace Bit.iOS.Core.Views
_mainIcon.Font = UIFont.FromName("bwi-font", 24);
_mainIcon.AdjustsFontSizeToFitWidth = true;
+ _mainIcon.Text = BitwardenIcons.Globe;
_mainIcon.TextColor = ThemeHelpers.PrimaryColor;
_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 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;
}
diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs
index f23c1df22..3cf336e66 100644
--- a/src/iOS.Core/Views/ExtensionTableSource.cs
+++ b/src/iOS.Core/Views/ExtensionTableSource.cs
@@ -1,6 +1,7 @@
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Resources.Localization;
+using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.iOS.Core.Controllers;
using Bit.iOS.Core.Models;
@@ -12,9 +13,9 @@ namespace Bit.iOS.Core.Views
{
public class ExtensionTableSource : ExtendedUITableViewSource
{
- public const string CellIdentifier = nameof(CipherLoginTableViewCell);
+ public const string CipherLoginCellIdentifier = nameof(CipherLoginTableViewCell);
- private IEnumerable _allItems = new List();
+ protected IEnumerable _allItems = new List();
protected ICipherService _cipherService;
protected ITotpService _totpService;
protected IStateService _stateService;
@@ -34,7 +35,7 @@ namespace Bit.iOS.Core.Views
Items = new List();
}
- public IEnumerable Items { get; private set; }
+ public IList Items { get; private set; }
public virtual async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
{
@@ -66,11 +67,11 @@ namespace Bit.iOS.Core.Views
return combinedLogins
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
- .Select(s => new CipherViewModel(s))
+ .Select(CreateCipherViewModel)
.ToList();
}
- public void FilterResults(string searchFilter, CancellationToken ct)
+ public virtual void FilterResults(string searchFilter, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
@@ -84,18 +85,24 @@ namespace Bit.iOS.Core.Views
var results = _searchService.SearchCiphersAsync(searchFilter,
c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted, null, ct)
.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)
{
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)
@@ -111,46 +118,51 @@ namespace Bit.iOS.Core.Views
return noDataCell;
}
- var cell = tableView.DequeueReusableCell(CellIdentifier);
+ var cell = tableView.DequeueReusableCell(CipherLoginCellIdentifier);
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;
}
public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
{
- if (Items == null
- || !Items.Any()
- || !(cell is CipherLoginTableViewCell cipherCell))
+ try
{
- return;
- }
+ if (Items == null
+ || !Items.Any()
+ || !(cell is CipherLoginTableViewCell cipherCell))
+ {
+ return;
+ }
- var item = Items.ElementAt(indexPath.Row);
- if (item is null)
- {
- return;
- }
+ var item = Items.ElementAtOrDefault(GetIndexForItemAt(tableView, indexPath));
+ if (item is null)
+ {
+ return;
+ }
- cipherCell.SetTitle(item.Name);
- cipherCell.SetSubtitle(item.Username);
- cipherCell.SetHasFido2Credential(item.HasFido2Credential);
- if (item.IsShared)
+ cipherCell.SetTitle(item.Name);
+ cipherCell.SetSubtitle(item.Username);
+ cipherCell.UpdateMainIcon(ShouldUseMainIconAsPasskey(item, indexPath));
+ 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)
{
- if (Items == null
- || !Items.Any())
- {
- return base.GetHeightForRow(tableView, indexPath);
- }
-
return 55;
}