mirror of
https://github.com/bitwarden/mobile
synced 2025-12-27 05:33:23 +00:00
Compare commits
4 Commits
temp-deleg
...
temp-passk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3568a5bb7 | ||
|
|
25f63ec4e0 | ||
|
|
c11df272be | ||
|
|
a4adcdcfd4 |
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.View;
|
||||
@@ -37,5 +34,6 @@ namespace Bit.Core.Abstractions
|
||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
||||
Task SoftDeleteWithServerAsync(string id);
|
||||
Task RestoreWithServerAsync(string id);
|
||||
Task<string> CreateNewLoginForPasskeyAsync(string rpId);
|
||||
}
|
||||
}
|
||||
|
||||
117
src/Core/Resources/Localization/AppResources.Designer.cs
generated
117
src/Core/Resources/Localization/AppResources.Designer.cs
generated
@@ -166,15 +166,6 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Credential Provider service
|
||||
/// </summary>
|
||||
public static string CredentialProviderService {
|
||||
get {
|
||||
return ResourceManager.GetString("CredentialProviderService", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden needs attention - See "Auto-fill Accessibility Service" from Bitwarden settings.
|
||||
/// </summary>
|
||||
@@ -1327,6 +1318,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings > System > Passwords & accounts > Passwords, passkeys and data services..
|
||||
/// </summary>
|
||||
public static string BitwardenCredentialProviderGoToSettings {
|
||||
get {
|
||||
return ResourceManager.GetString("BitwardenCredentialProviderGoToSettings", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden Help Center.
|
||||
/// </summary>
|
||||
@@ -1543,6 +1543,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Choose a login to save this passkey to.
|
||||
/// </summary>
|
||||
public static string ChooseALoginToSaveThisPasskeyTo {
|
||||
get {
|
||||
return ResourceManager.GetString("ChooseALoginToSaveThisPasskeyTo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Choose file.
|
||||
/// </summary>
|
||||
@@ -1903,6 +1912,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Credential Provider service.
|
||||
/// </summary>
|
||||
public static string CredentialProviderService {
|
||||
get {
|
||||
return ResourceManager.GetString("CredentialProviderService", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device..
|
||||
/// </summary>
|
||||
public static string CredentialProviderServiceExplanationLong {
|
||||
get {
|
||||
return ResourceManager.GetString("CredentialProviderServiceExplanationLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Credits.
|
||||
/// </summary>
|
||||
@@ -5110,6 +5137,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Overwrite passkey?.
|
||||
/// </summary>
|
||||
public static string OverwritePasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("OverwritePasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ownership.
|
||||
/// </summary>
|
||||
@@ -5137,6 +5173,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passkeys for {0}.
|
||||
/// </summary>
|
||||
public static string PasskeysForX {
|
||||
get {
|
||||
return ResourceManager.GetString("PasskeysForX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passkey will not be copied.
|
||||
/// </summary>
|
||||
@@ -5326,6 +5371,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passwords for {0}.
|
||||
/// </summary>
|
||||
public static string PasswordsForX {
|
||||
get {
|
||||
return ResourceManager.GetString("PasswordsForX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Password type.
|
||||
/// </summary>
|
||||
@@ -5831,6 +5885,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save passkey.
|
||||
/// </summary>
|
||||
public static string SavePasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("SavePasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save passkey as new login.
|
||||
/// </summary>
|
||||
public static string SavePasskeyAsNewLogin {
|
||||
get {
|
||||
return ResourceManager.GetString("SavePasskeyAsNewLogin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Saving....
|
||||
/// </summary>
|
||||
@@ -6704,6 +6776,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This item already contains a passkey. Are you sure you want to overwrite the current passkey?.
|
||||
/// </summary>
|
||||
public static string ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This request is no longer valid.
|
||||
/// </summary>
|
||||
@@ -7694,15 +7775,6 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings > System > Passwords & accounts > Passwords, passkeys and data services.
|
||||
/// </summary>
|
||||
public static string BitwardenCredentialProviderGoToSettings {
|
||||
get {
|
||||
return ResourceManager.GetString("BitwardenCredentialProviderGoToSettings", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Word separator.
|
||||
/// </summary>
|
||||
@@ -7721,15 +7793,6 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device.
|
||||
/// </summary>
|
||||
public static string CredentialProviderServiceExplanationLong {
|
||||
get {
|
||||
return ResourceManager.GetString("CredentialProviderServiceExplanationLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} hours and one minute.
|
||||
/// </summary>
|
||||
|
||||
@@ -2886,4 +2886,25 @@ Do you want to switch to this account?</value>
|
||||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||
</data>
|
||||
<data name="ChooseALoginToSaveThisPasskeyTo" xml:space="preserve">
|
||||
<value>Choose a login to save this passkey to</value>
|
||||
</data>
|
||||
<data name="SavePasskeyAsNewLogin" xml:space="preserve">
|
||||
<value>Save passkey as new login</value>
|
||||
</data>
|
||||
<data name="SavePasskey" xml:space="preserve">
|
||||
<value>Save passkey</value>
|
||||
</data>
|
||||
<data name="PasskeysForX" xml:space="preserve">
|
||||
<value>Passkeys for {0}</value>
|
||||
</data>
|
||||
<data name="PasswordsForX" xml:space="preserve">
|
||||
<value>Passwords for {0}</value>
|
||||
</data>
|
||||
<data name="OverwritePasskey" xml:space="preserve">
|
||||
<value>Overwrite passkey?</value>
|
||||
</data>
|
||||
<data name="ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey" xml:space="preserve">
|
||||
<value>This item already contains a passkey. Are you sure you want to overwrite the current passkey?</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1286,6 +1286,34 @@ namespace Bit.Core.Services
|
||||
cipher.PasswordHistory = encPhs;
|
||||
}
|
||||
|
||||
public async Task<string> CreateNewLoginForPasskeyAsync(string rpId)
|
||||
{
|
||||
var newCipher = new CipherView
|
||||
{
|
||||
Name = rpId,
|
||||
Type = CipherType.Login,
|
||||
Login = new LoginView
|
||||
{
|
||||
Uris = new List<LoginUriView>
|
||||
{
|
||||
new LoginUriView { Uri = rpId }
|
||||
}
|
||||
},
|
||||
Card = new CardView(),
|
||||
Identity = new IdentityView(),
|
||||
SecureNote = new SecureNoteView
|
||||
{
|
||||
Type = SecureNoteType.Generic
|
||||
},
|
||||
Reprompt = CipherRepromptType.None
|
||||
};
|
||||
|
||||
var encryptedCipher = await EncryptAsync(newCipher);
|
||||
await SaveWithServerAsync(encryptedCipher);
|
||||
|
||||
return encryptedCipher.Id;
|
||||
}
|
||||
|
||||
private class CipherLocaleComparer : IComparer<CipherView>
|
||||
{
|
||||
private readonly II18nService _i18nService;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// This implementation is used when all interactions are delegated to the operating system.
|
||||
/// Most often these decisions have already been made by the time the Authenticator is called.
|
||||
///
|
||||
/// This is only supported for assertion operations. Attestation requires the user to interact
|
||||
/// with the app directly.
|
||||
/// </summary>
|
||||
public class Fido2DelegatedUserInterface : IFido2UserInterface
|
||||
{
|
||||
private string _cipherId = null;
|
||||
private bool _userVerified = false;
|
||||
private Func<Task> _ensureUnlockedVaultAsyncCallback;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the user has already picked a credential from a list of existing credentials.
|
||||
/// Picking a credential also assumes user presence.
|
||||
/// </summary>
|
||||
public Fido2DelegatedUserInterface UserPickedCredential(string cipherId)
|
||||
{
|
||||
_cipherId = cipherId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the user was verified by the OS, e.g. by a fingerprint or face scan.
|
||||
/// </summary>
|
||||
public Fido2DelegatedUserInterface UserIsVerified()
|
||||
{
|
||||
_userVerified = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Fido2DelegatedUserInterface WithEnsureUnlockedVaultAsyncCallback(Func<Task> callback)
|
||||
{
|
||||
_ensureUnlockedVaultAsyncCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Task<Fido2PickCredentialResult> PickCredentialAsync(Fido2PickCredentialParams parameters)
|
||||
{
|
||||
return Task.FromResult(new Fido2PickCredentialResult
|
||||
{
|
||||
CipherId = _cipherId,
|
||||
UserVerified = _userVerified
|
||||
});
|
||||
}
|
||||
|
||||
public Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
if (_ensureUnlockedVaultAsyncCallback != null)
|
||||
{
|
||||
return _ensureUnlockedVaultAsyncCallback();
|
||||
}
|
||||
|
||||
throw new Exception("No callback provided to ensure the vault is unlocked");
|
||||
}
|
||||
|
||||
public Task InformExcludedCredential(string[] existingCipherIds) => throw new NotImplementedException();
|
||||
public Task<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams parameters) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
8
src/iOS.Autofill/ColorConstants.cs
Normal file
8
src/iOS.Autofill/ColorConstants.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public static class ColorConstants
|
||||
{
|
||||
public const string LIGHT_SECONDARY_300 = "LightSecondary300";
|
||||
public const string LIGHT_TEXT_MUTED = "LightTextMuted";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
@@ -19,10 +18,6 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost, IFido2UserInterface
|
||||
{
|
||||
private readonly LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
|
||||
private readonly Fido2DelegatedUserInterface _userInterface = new Fido2DelegatedUserInterface();
|
||||
|
||||
private IFido2AuthenticatorService _fido2AuthService;
|
||||
private IFido2AuthenticatorService Fido2AuthService
|
||||
{
|
||||
@@ -31,7 +26,7 @@ namespace Bit.iOS.Autofill
|
||||
if (_fido2AuthService is null)
|
||||
{
|
||||
_fido2AuthService = ServiceContainer.Resolve<IFido2AuthenticatorService>();
|
||||
_fido2AuthService.Init(_userInterface);
|
||||
_fido2AuthService.Init(this);
|
||||
}
|
||||
return _fido2AuthService;
|
||||
}
|
||||
@@ -90,6 +85,8 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
var credIdentity = Runtime.GetNSObject<ASPasskeyCredentialIdentity>(passkeyRegistrationRequest.CredentialIdentity.GetHandle());
|
||||
|
||||
_context.UrlString = credIdentity?.RelyingPartyIdentifier;
|
||||
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync");
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync RpID: {credIdentity.RelyingPartyIdentifier}");
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync UserName: {credIdentity.UserName}");
|
||||
@@ -161,20 +158,18 @@ namespace Bit.iOS.Autofill
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public class UserInteractionRequiredException : Exception {}
|
||||
|
||||
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
_context.PasskeyCredentialRequest = passkeyCredentialRequest;
|
||||
|
||||
try {
|
||||
await ProvideCredentialAsync(false);
|
||||
} catch (UserInteractionRequiredException) {
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
return;
|
||||
}
|
||||
_context.PasskeyCredentialRequest = passkeyCredentialRequest;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
|
||||
public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId)
|
||||
@@ -213,10 +208,6 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
});
|
||||
|
||||
_userInterface.UserPickedCredential(cipherId);
|
||||
// if (os.PerformedFaceID) {
|
||||
_userInterface.UserIsVerified();
|
||||
//}
|
||||
|
||||
ClipLogger.Log("fido2AssertionResult:" + fido2AssertionResult);
|
||||
|
||||
@@ -253,14 +244,14 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential");
|
||||
if (assertionCredential is null)
|
||||
{
|
||||
ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> assertionCredential is null");
|
||||
ServiceContainer.Reset();
|
||||
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||
return;
|
||||
}
|
||||
ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential");
|
||||
if (assertionCredential is null)
|
||||
{
|
||||
ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> assertionCredential is null");
|
||||
ServiceContainer.Reset();
|
||||
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||
return;
|
||||
}
|
||||
|
||||
//NSRunLoop.Main.BeginInvokeOnMainThread(() =>
|
||||
//{
|
||||
@@ -298,43 +289,28 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
public Task InformExcludedCredential(string[] existingCipherIds)
|
||||
{
|
||||
// iOS doesn't seem to provide the ExcludeCredentialDescriptorList so nothing to do here currently.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
|
||||
{
|
||||
// TODO: Show interface so the user can choose whether to create a new passkey or select one to add the passkey to.
|
||||
var newCipher = new CipherView
|
||||
ClipLogger.Log($"ConfirmNewCredentialAsync");
|
||||
_context.ConfirmNewCredentialTcs?.SetCanceled();
|
||||
_context.ConfirmNewCredentialTcs = new TaskCompletionSource<Fido2ConfirmNewCredentialResult>();
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
Name = confirmNewCredentialParams.RpId,
|
||||
Type = Bit.Core.Enums.CipherType.Login,
|
||||
Login = new LoginView
|
||||
try
|
||||
{
|
||||
Uris = new List<LoginUriView>
|
||||
{
|
||||
new LoginUriView
|
||||
{
|
||||
Uri = confirmNewCredentialParams.RpId
|
||||
}
|
||||
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
||||
}
|
||||
},
|
||||
Card = new CardView(),
|
||||
Identity = new IdentityView(),
|
||||
SecureNote = new SecureNoteView
|
||||
catch (Exception ex)
|
||||
{
|
||||
Type = Bit.Core.Enums.SecureNoteType.Generic
|
||||
},
|
||||
Reprompt = Bit.Core.Enums.CipherRepromptType.None
|
||||
};
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
});
|
||||
|
||||
var encryptedCipher = await _cipherService.Value.EncryptAsync(newCipher);
|
||||
await _cipherService.Value.SaveWithServerAsync(encryptedCipher);
|
||||
|
||||
return new Fido2ConfirmNewCredentialResult
|
||||
{
|
||||
CipherId = encryptedCipher.Id,
|
||||
UserVerified = true
|
||||
};
|
||||
return await _context.ConfirmNewCredentialTcs.Task;
|
||||
}
|
||||
|
||||
public async Task EnsureUnlockedVaultAsync()
|
||||
@@ -348,8 +324,8 @@ namespace Bit.iOS.Autofill
|
||||
return;
|
||||
}
|
||||
|
||||
_context._unlockVaultTcs?.SetCanceled();
|
||||
_context._unlockVaultTcs = new TaskCompletionSource<bool>();
|
||||
_context.UnlockVaultTcs?.SetCanceled();
|
||||
_context.UnlockVaultTcs = new TaskCompletionSource<bool>();
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
try
|
||||
@@ -364,7 +340,7 @@ namespace Bit.iOS.Autofill
|
||||
});
|
||||
|
||||
ClipLogger.Log($"EnsureUnlockedVaultAsync awaiting for unlock");
|
||||
await _context._unlockVaultTcs.Task;
|
||||
await _context.UnlockVaultTcs.Task;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -394,9 +394,16 @@ namespace Bit.iOS.Autofill
|
||||
CancelRequest(ASExtensionErrorCode.Failed);
|
||||
}
|
||||
|
||||
private void CancelRequest(ASExtensionErrorCode code)
|
||||
public void CancelRequest(ASExtensionErrorCode code)
|
||||
{
|
||||
ClipLogger.Log("CancelRequest" + code);
|
||||
|
||||
if (_context?.IsPasskey == true)
|
||||
{
|
||||
_context.ConfirmNewCredentialTcs?.TrySetCanceled();
|
||||
_context.UnlockVaultTcs?.TrySetCanceled();
|
||||
}
|
||||
|
||||
//var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null);
|
||||
var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
@@ -470,7 +477,7 @@ namespace Bit.iOS.Autofill
|
||||
if (_context.IsCreatingPasskey)
|
||||
{
|
||||
ClipLogger.Log("OnLockDismissedAsync -> IsCreatingPasskey");
|
||||
_context._unlockVaultTcs.SetResult(true);
|
||||
_context.UnlockVaultTcs.SetResult(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -506,13 +513,6 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private async Task ProvideCredentialAsync(bool userInteraction = true)
|
||||
{
|
||||
_userInterface.WithEnsureUnlockedVaultAsyncCallback(async () => {
|
||||
if (!userInteraction && (!await IsAuthed() || await IsLocked())) {
|
||||
throw new UserInteractionRequiredException();
|
||||
}
|
||||
|
||||
await EnsureUnlockedVaultAsync();
|
||||
});
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("ProvideCredentialAsync");
|
||||
|
||||
10
src/iOS.Autofill/ILoginListViewController.cs
Normal file
10
src/iOS.Autofill/ILoginListViewController.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Bit.iOS.Autofill.Models;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public interface ILoginListViewController
|
||||
{
|
||||
Context Context { get; }
|
||||
CredentialProviderViewController CPViewController { get; }
|
||||
}
|
||||
}
|
||||
59
src/iOS.Autofill/ListItems/HeaderItemView.cs
Normal file
59
src/iOS.Autofill/ListItems/HeaderItemView.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Bit.Core.Services;
|
||||
using Foundation;
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill.ListItems
|
||||
{
|
||||
public class HeaderItemView : UITableViewHeaderFooterView
|
||||
{
|
||||
private readonly UILabel _header = new UILabel();
|
||||
private readonly UIView _separator = new UIView();
|
||||
|
||||
public HeaderItemView(NSString reuseIdentifier)
|
||||
: base(reuseIdentifier)
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
protected internal HeaderItemView(NativeHandle handle) : base(handle)
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
public void SetHeaderText(string text) => _header.Text = text;
|
||||
|
||||
private void Setup()
|
||||
{
|
||||
try
|
||||
{
|
||||
_header.TextColor = UIColor.FromName(ColorConstants.LIGHT_TEXT_MUTED);
|
||||
_header.Font = UIFont.SystemFontOfSize(15);
|
||||
_separator.BackgroundColor = UIColor.FromName(ColorConstants.LIGHT_SECONDARY_300);
|
||||
|
||||
_header.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||
_separator.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||
|
||||
ContentView.AddSubview(_header);
|
||||
ContentView.AddSubview(_separator);
|
||||
|
||||
NSLayoutConstraint.ActivateConstraints(new NSLayoutConstraint[]
|
||||
{
|
||||
_header.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 9),
|
||||
_header.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 9),
|
||||
_header.TopAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TopAnchor, 3),
|
||||
|
||||
_separator.HeightAnchor.ConstraintEqualTo(2),
|
||||
_separator.TopAnchor.ConstraintEqualTo(_header.BottomAnchor, 8),
|
||||
_separator.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 5),
|
||||
_separator.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 5),
|
||||
_separator.BottomAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.BottomAnchor, 2)
|
||||
});
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Abstractions;
|
||||
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.Autofill.Utilities;
|
||||
using Bit.iOS.Core.Controllers;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.iOS.Core.Views;
|
||||
using CoreFoundation;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public partial class LoginListViewController : ExtendedUIViewController
|
||||
public partial class LoginListViewController : ExtendedUIViewController, ILoginListViewController
|
||||
{
|
||||
internal const string HEADER_SECTION_IDENTIFIER = "headerSectionId";
|
||||
|
||||
UIBarButtonItem _cancelButton;
|
||||
UIControl _accountSwitchButton;
|
||||
|
||||
@@ -24,59 +30,92 @@ namespace Bit.iOS.Autofill
|
||||
: base(handle)
|
||||
{
|
||||
DismissModalAction = Cancel;
|
||||
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
}
|
||||
|
||||
public Context Context { get; set; }
|
||||
public CredentialProviderViewController CPViewController { get; set; }
|
||||
public IPasswordRepromptService PasswordRepromptService { get; private set; }
|
||||
|
||||
AccountSwitchingOverlayView _accountSwitchingOverlayView;
|
||||
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
|
||||
|
||||
LazyResolve<IBroadcasterService> _broadcasterService = new LazyResolve<IBroadcasterService>("broadcasterService");
|
||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
LazyResolve<IBroadcasterService> _broadcasterService = new LazyResolve<IBroadcasterService>();
|
||||
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
bool _alreadyLoadItemsOnce = false;
|
||||
|
||||
public async override void ViewDidLoad()
|
||||
{
|
||||
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
SubscribeSyncCompleted();
|
||||
|
||||
NavItem.Title = AppResources.Items;
|
||||
_cancelButton.Title = AppResources.Cancel;
|
||||
|
||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||
TableView.EstimatedRowHeight = 44;
|
||||
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||
TableView.Source = new TableSource(this);
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
|
||||
_alreadyLoadItemsOnce = true;
|
||||
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var needsAutofillReplacement = await storageService.GetAsync<bool?>(
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
try
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
SubscribeSyncCompleted();
|
||||
|
||||
NavItem.Title = Context.IsCreatingPasskey ? AppResources.SavePasskey : AppResources.Items;
|
||||
_cancelButton.Title = AppResources.Cancel;
|
||||
|
||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||
TableView.EstimatedRowHeight = 44;
|
||||
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||
TableView.Source = new TableSource(this);
|
||||
if (Context.IsCreatingPasskey)
|
||||
{
|
||||
TableView.SectionHeaderHeight = 55;
|
||||
TableView.RegisterClassForHeaderFooterViewReuse(typeof(HeaderItemView), HEADER_SECTION_IDENTIFIER);
|
||||
}
|
||||
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(15, 0))
|
||||
{
|
||||
TableView.SectionHeaderTopPadding = 0;
|
||||
}
|
||||
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
|
||||
if (Context.IsCreatingPasskey)
|
||||
{
|
||||
_headerLabel.Text = AppResources.ChooseALoginToSaveThisPasskeyTo;
|
||||
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, Context.UrlString);
|
||||
|
||||
_emptyViewButton.SetTitle(AppResources.SavePasskeyAsNewLogin, UIControlState.Normal);
|
||||
_emptyViewButton.Layer.BorderWidth = 2;
|
||||
_emptyViewButton.Layer.BorderColor = UIColor.FromName(ColorConstants.LIGHT_TEXT_MUTED).CGColor;
|
||||
_emptyViewButton.Layer.CornerRadius = 10;
|
||||
_emptyViewButton.ClipsToBounds = true;
|
||||
|
||||
_headerView.Hidden = false;
|
||||
}
|
||||
|
||||
_alreadyLoadItemsOnce = true;
|
||||
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var needsAutofillReplacement = await storageService.GetAsync<bool?>(
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
|
||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||
|
||||
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
|
||||
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
|
||||
|
||||
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
|
||||
{
|
||||
_cancelButton,
|
||||
new UIBarButtonItem(_accountSwitchButton)
|
||||
}, false);
|
||||
|
||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||
}
|
||||
|
||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||
|
||||
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
|
||||
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
|
||||
|
||||
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
|
||||
catch (Exception ex)
|
||||
{
|
||||
_cancelButton,
|
||||
new UIBarButtonItem(_accountSwitchButton)
|
||||
}, false);
|
||||
|
||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_TouchUpInside(object sender, EventArgs e)
|
||||
@@ -91,17 +130,41 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
CPViewController.CompleteRequest();
|
||||
CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled);
|
||||
}
|
||||
|
||||
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
PerformSegue("loginAddSegue", this);
|
||||
PerformSegue(SegueConstants.ADD_LOGIN, this);
|
||||
}
|
||||
|
||||
partial void SearchBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
PerformSegue("loginSearchFromListSegue", this);
|
||||
PerformSegue(SegueConstants.LOGIN_SEARCH_FROM_LIST, this);
|
||||
}
|
||||
|
||||
partial void EmptyButton_Activated(UIButton sender)
|
||||
{
|
||||
SavePasskeyAsNewLoginAsync().FireAndForget(ex =>
|
||||
{
|
||||
_platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred).FireAndForget();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SavePasskeyAsNewLoginAsync()
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
return;
|
||||
}
|
||||
|
||||
var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCredentialIdentity.RelyingPartyIdentifier);
|
||||
Context.ConfirmNewCredentialTcs.TrySetResult(new Fido2ConfirmNewCredentialResult
|
||||
{
|
||||
CipherId = cipherId,
|
||||
UserVerified = true
|
||||
});
|
||||
}
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
@@ -136,7 +199,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
TableView.ReloadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -148,6 +211,13 @@ namespace Bit.iOS.Autofill
|
||||
});
|
||||
}
|
||||
|
||||
public void OnEmptyList()
|
||||
{
|
||||
_emptyView.Hidden = false;
|
||||
_headerView.Hidden = false;
|
||||
TableView.Hidden = true;
|
||||
}
|
||||
|
||||
public override void ViewDidUnload()
|
||||
{
|
||||
base.ViewDidUnload();
|
||||
@@ -159,8 +229,15 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
DismissViewController(true, async () =>
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
TableView.ReloadData();
|
||||
try
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
TableView.ReloadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,20 +256,61 @@ namespace Bit.iOS.Autofill
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public class TableSource : ExtensionTableSource
|
||||
public class TableSource : BaseLoginListTableSource<LoginListViewController>
|
||||
{
|
||||
private LoginListViewController _controller;
|
||||
|
||||
public TableSource(LoginListViewController controller)
|
||||
: base(controller.Context, controller)
|
||||
: base(controller)
|
||||
{
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN;
|
||||
|
||||
public override async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
|
||||
{
|
||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
||||
_controller.CPViewController, _controller, _controller.PasswordRepromptService, "loginAddSegue");
|
||||
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);// base.GetViewForHeader(tableView, section);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
src/iOS.Autofill/LoginListViewController.designer.cs
generated
51
src/iOS.Autofill/LoginListViewController.designer.cs
generated
@@ -12,6 +12,24 @@ namespace Bit.iOS.Autofill
|
||||
[Register ("LoginListViewController")]
|
||||
partial class LoginListViewController
|
||||
{
|
||||
[Outlet]
|
||||
UIKit.UIView _emptyView { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UIButton _emptyViewButton { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UIImageView _emptyViewImage { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UILabel _emptyViewLabel { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UILabel _headerLabel { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UIView _headerView { get; set; }
|
||||
|
||||
[Outlet]
|
||||
[GeneratedCode ("iOS Designer", "1.0")]
|
||||
UIKit.UIBarButtonItem AddBarButton { get; set; }
|
||||
@@ -32,11 +50,44 @@ namespace Bit.iOS.Autofill
|
||||
[Action ("AddBarButton_Activated:")]
|
||||
partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender);
|
||||
|
||||
[Action ("EmptyButton_Activated:")]
|
||||
partial void EmptyButton_Activated (UIKit.UIButton sender);
|
||||
|
||||
[Action ("SearchBarButton_Activated:")]
|
||||
partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender);
|
||||
|
||||
void ReleaseDesignerOutlets ()
|
||||
{
|
||||
if (_emptyView != null) {
|
||||
_emptyView.Dispose ();
|
||||
_emptyView = null;
|
||||
}
|
||||
|
||||
if (_emptyViewButton != null) {
|
||||
_emptyViewButton.Dispose ();
|
||||
_emptyViewButton = null;
|
||||
}
|
||||
|
||||
if (_emptyViewImage != null) {
|
||||
_emptyViewImage.Dispose ();
|
||||
_emptyViewImage = null;
|
||||
}
|
||||
|
||||
if (_emptyViewLabel != null) {
|
||||
_emptyViewLabel.Dispose ();
|
||||
_emptyViewLabel = null;
|
||||
}
|
||||
|
||||
if (_headerLabel != null) {
|
||||
_headerLabel.Dispose ();
|
||||
_headerLabel = null;
|
||||
}
|
||||
|
||||
if (_headerView != null) {
|
||||
_headerView.Dispose ();
|
||||
_headerView = null;
|
||||
}
|
||||
|
||||
if (AddBarButton != null) {
|
||||
AddBarButton.Dispose ();
|
||||
AddBarButton = null;
|
||||
|
||||
@@ -12,19 +12,17 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public partial class LoginSearchViewController : ExtendedUITableViewController
|
||||
public partial class LoginSearchViewController : ExtendedUITableViewController, ILoginListViewController
|
||||
{
|
||||
public LoginSearchViewController(IntPtr handle)
|
||||
: base(handle)
|
||||
{
|
||||
DismissModalAction = Cancel;
|
||||
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
}
|
||||
|
||||
public Context Context { get; set; }
|
||||
public CredentialProviderViewController CPViewController { get; set; }
|
||||
public bool FromList { get; set; }
|
||||
public IPasswordRepromptService PasswordRepromptService { get; private set; }
|
||||
|
||||
public async override void ViewDidLoad()
|
||||
{
|
||||
@@ -39,7 +37,7 @@ namespace Bit.iOS.Autofill
|
||||
TableView.EstimatedRowHeight = 44;
|
||||
TableView.Source = new TableSource(this);
|
||||
SearchBar.Delegate = new ExtensionSearchDelegate(TableView);
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text);
|
||||
await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
||||
}
|
||||
|
||||
public override void ViewDidAppear(bool animated)
|
||||
@@ -61,13 +59,13 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
else
|
||||
{
|
||||
CPViewController.CompleteRequest();
|
||||
CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled);
|
||||
}
|
||||
}
|
||||
|
||||
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
PerformSegue("loginAddFromSearchSegue", this);
|
||||
PerformSegue(SegueConstants.ADD_LOGIN_FROM_SEARCH, this);
|
||||
}
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
@@ -88,29 +86,19 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
DismissViewController(true, async () =>
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text);
|
||||
await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
||||
TableView.ReloadData();
|
||||
});
|
||||
}
|
||||
|
||||
public class TableSource : ExtensionTableSource
|
||||
public class TableSource : BaseLoginListTableSource<LoginSearchViewController>
|
||||
{
|
||||
private Context _context;
|
||||
private LoginSearchViewController _controller;
|
||||
|
||||
public TableSource(LoginSearchViewController controller)
|
||||
: base(controller.Context, controller)
|
||||
: base(controller)
|
||||
{
|
||||
_context = controller.Context;
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
||||
_controller.CPViewController, _controller, _controller.PasswordRepromptService,
|
||||
"loginAddFromSearchSegue");
|
||||
}
|
||||
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN_FROM_SEARCH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -131,7 +132,75 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="830"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="2305">
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bAz-MO-Wzd" userLabel="HeaderView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="39.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ngG-eh-mSP" userLabel="HeaderLabel">
|
||||
<rect key="frame" x="18" y="12" width="378" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="V7p-E8-7fp" userLabel="SeparatorView">
|
||||
<rect key="frame" x="9" y="37.5" width="396" height="2"/>
|
||||
<color key="backgroundColor" name="LightSecondary300"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="2" id="5pj-pp-Crd"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="V7p-E8-7fp" secondAttribute="trailing" constant="9" id="5vq-UE-Ebp"/>
|
||||
<constraint firstAttribute="bottom" secondItem="V7p-E8-7fp" secondAttribute="bottom" id="8Jj-Cy-WcG"/>
|
||||
<constraint firstItem="V7p-E8-7fp" firstAttribute="leading" secondItem="bAz-MO-Wzd" secondAttribute="leading" constant="9" id="cmb-sZ-Oar"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ngG-eh-mSP" secondAttribute="trailing" constant="18" id="f14-Hv-ajq"/>
|
||||
<constraint firstItem="ngG-eh-mSP" firstAttribute="leading" secondItem="bAz-MO-Wzd" secondAttribute="leading" constant="18" id="htJ-47-GLg"/>
|
||||
<constraint firstItem="V7p-E8-7fp" firstAttribute="top" secondItem="ngG-eh-mSP" secondAttribute="bottom" constant="5" id="rdq-1s-mfF"/>
|
||||
<constraint firstItem="ngG-eh-mSP" firstAttribute="top" secondItem="bAz-MO-Wzd" secondAttribute="top" constant="12" id="sCw-FM-uEg"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wNm-Sy-bJv" userLabel="EmptyView">
|
||||
<rect key="frame" x="0.0" y="139.5" width="414" height="228"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="empty_items_state" translatesAutoresizingMaskIntoConstraints="NO" id="FDN-Dp-jl3">
|
||||
<rect key="frame" x="128.5" y="0.0" width="157" height="110.5"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ThereAreNoItemsInYourVaultForX" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tEp-qe-xvE">
|
||||
<rect key="frame" x="19" y="125.5" width="376" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" name="LightTextMuted"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Gv5-Xt-G9l" userLabel="EmptyButton">
|
||||
<rect key="frame" x="19" y="186" width="376" height="42"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="42" id="9gk-Kj-BzZ"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="plain" title="Action">
|
||||
<fontDescription key="titleFontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="16"/>
|
||||
<color key="baseForegroundColor" name="LightTextMuted"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="EmptyButton_Activated:" destination="2304" eventType="touchUpInside" id="AvC-7H-cda"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Gv5-Xt-G9l" secondAttribute="trailing" constant="19" id="CAs-sg-RdA"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tEp-qe-xvE" secondAttribute="trailing" constant="19" id="FcV-36-N23"/>
|
||||
<constraint firstItem="tEp-qe-xvE" firstAttribute="leading" secondItem="wNm-Sy-bJv" secondAttribute="leading" constant="19" id="FuK-Iy-WB7"/>
|
||||
<constraint firstItem="FDN-Dp-jl3" firstAttribute="centerX" secondItem="wNm-Sy-bJv" secondAttribute="centerX" id="QY2-iP-E6Z"/>
|
||||
<constraint firstItem="Gv5-Xt-G9l" firstAttribute="leading" secondItem="wNm-Sy-bJv" secondAttribute="leading" constant="19" id="X1L-jb-zcg"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Gv5-Xt-G9l" secondAttribute="bottom" id="Z0q-gg-lmn"/>
|
||||
<constraint firstItem="tEp-qe-xvE" firstAttribute="top" secondItem="FDN-Dp-jl3" secondAttribute="bottom" constant="15" id="iwh-e6-yhe"/>
|
||||
<constraint firstItem="Gv5-Xt-G9l" firstAttribute="top" secondItem="tEp-qe-xvE" secondAttribute="bottom" constant="40" id="jkX-tr-T2A"/>
|
||||
<constraint firstItem="FDN-Dp-jl3" firstAttribute="top" secondItem="wNm-Sy-bJv" secondAttribute="top" id="kKX-UE-JzG"/>
|
||||
</constraints>
|
||||
</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">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="781"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<prototypes>
|
||||
@@ -173,13 +242,20 @@
|
||||
<viewLayoutGuide key="safeArea" id="BQW-dG-XMM"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="wNm-Sy-bJv" secondAttribute="bottom" constant="200" id="42A-4V-UIl"/>
|
||||
<constraint firstItem="Tq0-Ep-tHr" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="4wL-FF-CVk"/>
|
||||
<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="wNm-Sy-bJv" firstAttribute="top" secondItem="bAz-MO-Wzd" secondAttribute="bottom" constant="100" id="CWX-uT-sfH"/>
|
||||
<constraint firstItem="bAz-MO-Wzd" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="SBv-yF-WW2"/>
|
||||
<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="BQW-dG-XMM" firstAttribute="trailing" secondItem="2305" secondAttribute="trailing" id="ofJ-fL-adF"/>
|
||||
<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="bAz-MO-Wzd" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="uiV-Kh-8Iz"/>
|
||||
<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="bAz-MO-Wzd" secondAttribute="trailing" id="vC6-AI-wVU"/>
|
||||
<constraint firstItem="2305" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="xfQ-VQ-yWe"/>
|
||||
</constraints>
|
||||
</view>
|
||||
@@ -207,6 +283,12 @@
|
||||
<outlet property="NavItem" destination="3734" id="name-outlet-3734"/>
|
||||
<outlet property="OverlayView" destination="Tq0-Ep-tHr" id="igj-R2-gXJ"/>
|
||||
<outlet property="TableView" destination="2305" id="aUe-Uz-iIb"/>
|
||||
<outlet property="_emptyView" destination="wNm-Sy-bJv" id="Whk-C5-rjW"/>
|
||||
<outlet property="_emptyViewButton" destination="Gv5-Xt-G9l" id="JHd-sV-VJC"/>
|
||||
<outlet property="_emptyViewImage" destination="FDN-Dp-jl3" id="Dzb-p3-tv0"/>
|
||||
<outlet property="_emptyViewLabel" destination="tEp-qe-xvE" id="CPZ-it-kVY"/>
|
||||
<outlet property="_headerLabel" destination="ngG-eh-mSP" id="1bj-Ii-8OY"/>
|
||||
<outlet property="_headerView" destination="bAz-MO-Wzd" id="85r-XO-e5h"/>
|
||||
<segue destination="1845" kind="presentation" identifier="loginAddSegue" modalPresentationStyle="fullScreen" modalTransitionStyle="coverVertical" id="3731"/>
|
||||
<segue destination="11552" kind="show" identifier="loginSearchFromListSegue" id="12574"/>
|
||||
</connections>
|
||||
@@ -575,7 +657,14 @@
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="check.png" width="90" height="90"/>
|
||||
<image name="empty_items_state" width="157" height="111"/>
|
||||
<image name="logo.png" width="282" height="44"/>
|
||||
<namedColor name="LightSecondary300">
|
||||
<color red="0.80800002813339233" green="0.83099997043609619" blue="0.86299997568130493" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</namedColor>
|
||||
<namedColor name="LightTextMuted">
|
||||
<color red="0.42699998617172241" green="0.45899999141693115" blue="0.49399998784065247" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="darkTextColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.iOS.Core.Models;
|
||||
using Foundation;
|
||||
using ObjCRuntime;
|
||||
@@ -15,7 +16,8 @@ namespace Bit.iOS.Autofill.Models
|
||||
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
||||
public bool Configuring { get; set; }
|
||||
public bool IsCreatingPasskey { get; set; }
|
||||
public TaskCompletionSource<bool> _unlockVaultTcs { get; set; }
|
||||
public TaskCompletionSource<bool> UnlockVaultTcs { get; set; }
|
||||
public TaskCompletionSource<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialTcs { get; set; }
|
||||
|
||||
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
||||
{
|
||||
|
||||
6
src/iOS.Autofill/Resources/Assets.xcassets/Contents.json
Executable file
6
src/iOS.Autofill/Resources/Assets.xcassets/Contents.json
Executable file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.863",
|
||||
"green" : "0.831",
|
||||
"red" : "0.808"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.924",
|
||||
"green" : "0.879",
|
||||
"red" : "0.854"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.494",
|
||||
"green" : "0.459",
|
||||
"red" : "0.427"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.783",
|
||||
"green" : "0.718",
|
||||
"red" : "0.671"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
25
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
vendored
Executable file
25
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
vendored
Executable file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Empty-items-state.pdf",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Empty-items-state-dark.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
vendored
Executable file
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
vendored
Executable file
Binary file not shown.
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf
vendored
Executable file
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf
vendored
Executable file
Binary file not shown.
13
src/iOS.Autofill/SegueConstants.cs
Normal file
13
src/iOS.Autofill/SegueConstants.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public static class SegueConstants
|
||||
{
|
||||
public const string LOGIN_LIST = "loginListSegue";
|
||||
public const string LOCK = "lockPasswordSegue";
|
||||
public const string LOGIN_SEARCH = "loginSearchSegue";
|
||||
public const string SETUP = "setupSegue";
|
||||
public const string ADD_LOGIN = "loginAddSegue";
|
||||
public const string LOGIN_SEARCH_FROM_LIST = "loginSearchFromListSegue";
|
||||
public const string ADD_LOGIN_FROM_SEARCH = "loginAddFromSearchSegue";
|
||||
}
|
||||
}
|
||||
91
src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
Normal file
91
src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill.Utilities
|
||||
{
|
||||
public abstract class BaseLoginListTableSource<T> : ExtensionTableSource
|
||||
where T : UIViewController, ILoginListViewController
|
||||
{
|
||||
private IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
|
||||
public BaseLoginListTableSource(T controller)
|
||||
: base(controller.Context, controller)
|
||||
{
|
||||
_controller = controller;
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>();
|
||||
}
|
||||
|
||||
protected Context Context => (Context)_context;
|
||||
protected T Controller => (T)_controller;
|
||||
|
||||
protected abstract string LoginAddSegue { get; }
|
||||
|
||||
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Context.IsCreatingPasskey)
|
||||
{
|
||||
await SelectRowForPasskeyCreationAsync(tableView, indexPath);
|
||||
return;
|
||||
}
|
||||
|
||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
||||
Controller.CPViewController, Controller, _passwordRepromptService, LoginAddSegue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectRowForPasskeyCreationAsync(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
tableView.DeselectRow(indexPath, true);
|
||||
tableView.EndEditing(true);
|
||||
|
||||
var item = Items.ElementAt(indexPath.Row);
|
||||
if (item is null)
|
||||
{
|
||||
await _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.CipherView.Login.HasFido2Credentials
|
||||
&&
|
||||
!await _platformUtilsService.Value.ShowDialogAsync(
|
||||
AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey,
|
||||
AppResources.OverwritePasskey,
|
||||
AppResources.Yes,
|
||||
AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(item.Reprompt))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Check user verification
|
||||
|
||||
Context.ConfirmNewCredentialTcs.SetResult(new Fido2ConfirmNewCredentialResult
|
||||
{
|
||||
CipherId = item.Id,
|
||||
UserVerified = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
<ItemGroup>
|
||||
<TrimmerRootAssembly Include="System.Security.Cryptography" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="ListItems\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="CredentialProviderViewController.cs" />
|
||||
<Compile Include="CredentialProviderViewController.designer.cs">
|
||||
@@ -82,6 +85,11 @@
|
||||
<BundleResource Include="Resources\MaterialIcons_Regular.ttf" />
|
||||
<BundleResource Include="Resources\bwi-font.ttf" />
|
||||
<Compile Include="CredentialProviderViewController.Passkeys.cs" />
|
||||
<Compile Include="SegueConstants.cs" />
|
||||
<Compile Include="ColorConstants.cs" />
|
||||
<Compile Include="ListItems\HeaderItemView.cs" />
|
||||
<Compile Include="Utilities\BaseLoginListTableSource.cs" />
|
||||
<Compile Include="ILoginListViewController.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BundleResource Include="Resources\check.png" />
|
||||
@@ -180,4 +188,7 @@
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\iOS.Core\iOS.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="ListItems\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using System.Diagnostics;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Core.Controllers;
|
||||
using Bit.iOS.Core.Models;
|
||||
@@ -25,8 +20,8 @@ namespace Bit.iOS.Core.Views
|
||||
protected ITotpService _totpService;
|
||||
protected IStateService _stateService;
|
||||
protected ISearchService _searchService;
|
||||
private AppExtensionContext _context;
|
||||
private UIViewController _controller;
|
||||
protected AppExtensionContext _context;
|
||||
protected UIViewController _controller;
|
||||
|
||||
public ExtensionTableSource(AppExtensionContext context, UIViewController controller)
|
||||
{
|
||||
@@ -36,11 +31,19 @@ namespace Bit.iOS.Core.Views
|
||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
_context = context;
|
||||
_controller = controller;
|
||||
|
||||
Items = new List<CipherViewModel>();
|
||||
}
|
||||
|
||||
public IEnumerable<CipherViewModel> Items { get; private set; }
|
||||
|
||||
public async Task LoadItemsAsync(bool urlFilter = true, string searchFilter = null)
|
||||
public virtual async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
|
||||
{
|
||||
_allItems = await LoadItemsAsync(urlFilter, searchFilter);
|
||||
FilterResults(searchFilter, new CancellationToken());
|
||||
}
|
||||
|
||||
protected virtual async Task<IEnumerable<CipherViewModel>> LoadItemsAsync(bool urlFilter = true, string? searchFilter = null)
|
||||
{
|
||||
var combinedLogins = new List<CipherView>();
|
||||
|
||||
@@ -62,11 +65,10 @@ namespace Bit.iOS.Core.Views
|
||||
combinedLogins.AddRange(logins);
|
||||
}
|
||||
|
||||
_allItems = combinedLogins
|
||||
return combinedLogins
|
||||
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
|
||||
.Select(s => new CipherViewModel(s))
|
||||
.ToList() ?? new List<CipherViewModel>();
|
||||
FilterResults(searchFilter, new CancellationToken());
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void FilterResults(string searchFilter, CancellationToken ct)
|
||||
@@ -87,7 +89,7 @@ namespace Bit.iOS.Core.Views
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<CipherViewModel> TableItems { get; set; }
|
||||
//public IEnumerable<CipherViewModel> TableItems { get; set; }
|
||||
|
||||
public override nint RowsInSection(UITableView tableview, nint section)
|
||||
{
|
||||
@@ -135,9 +137,9 @@ namespace Bit.iOS.Core.Views
|
||||
cell.DetailTextLabel.Text = item.Username;
|
||||
}
|
||||
|
||||
public async Task<string> GetTotpAsync(CipherViewModel item)
|
||||
public async Task<string?> GetTotpAsync(CipherViewModel item)
|
||||
{
|
||||
string totp = null;
|
||||
string? totp = null;
|
||||
var accessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
if (accessPremium || (item?.CipherView.OrganizationUseTotp ?? false))
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Bit.iOS.Extension
|
||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||
TableView.EstimatedRowHeight = 44;
|
||||
TableView.Source = new TableSource(this);
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
}
|
||||
|
||||
public bool CanAutoFill()
|
||||
@@ -93,7 +93,7 @@ namespace Bit.iOS.Extension
|
||||
{
|
||||
DismissViewController(true, async () =>
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
TableView.ReloadData();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user