mirror of
https://github.com/bitwarden/mobile
synced 2025-12-19 09:43:27 +00:00
[PM-5154] Implement Passkeys on iOS (#3017)
* [PM-5731] feat: implement get assertion params object * [PM-5731] feat: add first test * [PM-5731] feat: add rp mismatch test * [PM-5731] feat: ask for credentials when found * [PM-5731] feat: find discoverable credentials * [PM-5731] feat: add tests for successful UV requests * [PM-5731] feat: add user does not consent test * [PM-5731] feat: check for UV when reprompt is active * [PM-5731] fix: tests a bit, needed some additional "arrange" steps * [PM-5731] feat: add support for counter * [PM-5731] feat: implement assertion without signature * [PM-5732] feat: finish authenticator assertion implementation note: CryptoFunctionService still needs Sign implemenation * [PM-5731] chore: minor clean up * [PM-5731] feat: scaffold make credential * [PM-5731] feat: start implementing attestation * [PM-5731] feat: implement credential exclusion * [PM-5731] feat: add new credential confirmaiton * [PM-5731] feat: implement credential creation * [PM-5731] feat: add user verification checks * [PM-5731] feat: add unknown error handling * [PM-5731] chore: clean up unusued params * [PM-5731] feat: partial attestation implementation * [PM-5731] feat: implement key generation * [PM-5731] feat: return public key in DER format * [PM-5731] feat: implement signing * [PM-5731] feat: remove logging * [PM-5731] chore: use primary constructor * [PM-5731] chore: add Async to method names * [PM-5731] feat: add support for silent discoverability * [PM-5731] feat: add support for specifying user presence requirement * [PM-5731] feat: ensure unlocked vault * [PM-5731] chore: clean up and refactor assertion tests * [PM-5731] chore: clean up and refactor attestation tests * [PM-5731] chore: add user presence todo comment * [PM-5731] feat: scaffold fido2 client * PM-5731 Fix build updating discoverable flag * [PM-5731] fix: failing test * [PM-5731] feat: add sameOriginWithAncestor and user id length checks * [PM-5731] feat: add incomplete rpId verification * [PM-5731] chore: document uri helpers * [PM-5731] feat: implement fido2 client createCredential * Added iOS passkeys integration, warning this branch has lots of logs to ease "debugging" extensions. * [PM-5731] feat: implement credential assertion in client * PM-5154 Fixed select passkey flow and started implementing create passkey on iOS * fix wrong signature format * PM-5154 [Passkeys iOS] Fix Credential ID handling on bytes and string formats. Fix Discoverable to be lowercase on set so it doesn't break parsing on clients. Added UserDisplayName on Fido2 entities. Extracted the Guid Standard/Raw format helpers to a extensions class. * Fix incompatible GUID conversions * PM-5154 [Passkeys iOS] Added custom UI flow for passkey creation * PM-5154 [Passkeys iOS] Updated UI for passkey creation * PM-5154 [Passkeys iOS] Refactored and added cipher selection for passkey creation on autofill search. * PM-5154 [Passkeys iOS] Fixed empty top space on autofill password list --------- Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> Co-authored-by: mpbw2 <59324545+mpbw2@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
71de3bedf4
commit
16e1b60a4d
@@ -1,6 +1,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<MauiVersion>8.0.4-nightly.*</MauiVersion>
|
<MauiVersion>8.0.7-nightly.*</MauiVersion>
|
||||||
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
||||||
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
||||||
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
@@ -37,5 +34,6 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
||||||
Task SoftDeleteWithServerAsync(string id);
|
Task SoftDeleteWithServerAsync(string id);
|
||||||
Task RestoreWithServerAsync(string id);
|
Task RestoreWithServerAsync(string id);
|
||||||
|
Task<string> CreateNewLoginForPasskeyAsync(string rpId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Domain;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
@@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
|
|||||||
RpName = fido2Key.RpName?.EncryptedString;
|
RpName = fido2Key.RpName?.EncryptedString;
|
||||||
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
||||||
UserName = fido2Key.UserName?.EncryptedString;
|
UserName = fido2Key.UserName?.EncryptedString;
|
||||||
|
UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
|
||||||
Counter = fido2Key.Counter?.EncryptedString;
|
Counter = fido2Key.Counter?.EncryptedString;
|
||||||
CreationDate = fido2Key.CreationDate;
|
CreationDate = fido2Key.CreationDate;
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
|
|||||||
RpName = apiData.RpName;
|
RpName = apiData.RpName;
|
||||||
UserHandle = apiData.UserHandle;
|
UserHandle = apiData.UserHandle;
|
||||||
UserName = apiData.UserName;
|
UserName = apiData.UserName;
|
||||||
|
UserDisplayName = apiData.UserDisplayName;
|
||||||
Counter = apiData.Counter;
|
Counter = apiData.Counter;
|
||||||
CreationDate = apiData.CreationDate;
|
CreationDate = apiData.CreationDate;
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Models.Data;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Domain
|
namespace Bit.Core.Models.Domain
|
||||||
@@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
nameof(RpName),
|
nameof(RpName),
|
||||||
nameof(UserHandle),
|
nameof(UserHandle),
|
||||||
nameof(UserName),
|
nameof(UserName),
|
||||||
|
nameof(UserDisplayName),
|
||||||
nameof(Counter)
|
nameof(Counter)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString RpName { get; set; }
|
public EncString RpName { get; set; }
|
||||||
public EncString UserHandle { get; set; }
|
public EncString UserHandle { get; set; }
|
||||||
public EncString UserName { get; set; }
|
public EncString UserName { get; set; }
|
||||||
|
public EncString UserDisplayName { get; set; }
|
||||||
public EncString Counter { get; set; }
|
public EncString Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Enums;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
@@ -25,32 +26,42 @@ namespace Bit.Core.Models.View
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public int CounterValue {
|
public int CounterValue {
|
||||||
get => int.TryParse(Counter, out var counter) ? counter : 0;
|
get => int.TryParse(Counter, out var counter) ? counter : 0;
|
||||||
set => Counter = value.ToString();
|
set => Counter = value.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public byte[] UserHandleValue {
|
public byte[] UserHandleValue {
|
||||||
get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
|
get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
|
||||||
set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public byte[] KeyBytes {
|
public byte[] KeyBytes {
|
||||||
get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
|
get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
|
||||||
set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public bool DiscoverableValue {
|
public bool DiscoverableValue {
|
||||||
get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
|
get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
|
||||||
set => Discoverable = value.ToString().ToLower();
|
set => Discoverable = value.ToString().ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public override string SubTitle => UserName;
|
public override string SubTitle => UserName;
|
||||||
|
|
||||||
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
|
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
|
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
|
||||||
|
[JsonIgnore]
|
||||||
public string LaunchUri => $"https://{RpId}";
|
public string LaunchUri => $"https://{RpId}";
|
||||||
|
|
||||||
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
||||||
|
|||||||
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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Bitwarden needs attention - See "Auto-fill Accessibility Service" from Bitwarden settings.
|
/// Looks up a localized string similar to Bitwarden needs attention - See "Auto-fill Accessibility Service" from Bitwarden settings.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Bitwarden Help Center.
|
/// Looks up a localized string similar to Bitwarden Help Center.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Choose file.
|
/// Looks up a localized string similar to Choose file.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Credits.
|
/// Looks up a localized string similar to Credits.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Ownership.
|
/// Looks up a localized string similar to Ownership.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Passkey will not be copied.
|
/// Looks up a localized string similar to Passkey will not be copied.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Password type.
|
/// Looks up a localized string similar to Password type.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Saving....
|
/// Looks up a localized string similar to Saving....
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to This request is no longer valid.
|
/// Looks up a localized string similar to This request is no longer valid.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Word separator.
|
/// Looks up a localized string similar to Word separator.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to {0} hours and one minute.
|
/// Looks up a localized string similar to {0} hours and one minute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2886,4 +2886,25 @@ Do you want to switch to this account?</value>
|
|||||||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|||||||
@@ -1286,6 +1286,34 @@ namespace Bit.Core.Services
|
|||||||
cipher.PasswordHistory = encPhs;
|
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 class CipherLocaleComparer : IComparer<CipherView>
|
||||||
{
|
{
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace Bit.Core.Services
|
|||||||
throw new NotAllowedError();
|
throw new NotAllowedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
cipher.Login.Fido2Credentials = [fido2Credential];
|
cipher.Login.Fido2Credentials = new List<Fido2CredentialView> { fido2Credential };
|
||||||
var reencrypted = await _cipherService.EncryptAsync(cipher);
|
var reencrypted = await _cipherService.EncryptAsync(cipher);
|
||||||
await _cipherService.SaveWithServerAsync(reencrypted);
|
await _cipherService.SaveWithServerAsync(reencrypted);
|
||||||
credentialId = fido2Credential.CredentialId;
|
credentialId = fido2Credential.CredentialId;
|
||||||
@@ -287,7 +287,7 @@ namespace Bit.Core.Services
|
|||||||
UserName = makeCredentialsParams.UserEntity.Name,
|
UserName = makeCredentialsParams.UserEntity.Name,
|
||||||
CounterValue = 0,
|
CounterValue = 0,
|
||||||
RpName = makeCredentialsParams.RpEntity.Name,
|
RpName = makeCredentialsParams.RpEntity.Name,
|
||||||
// UserDisplayName = makeCredentialsParams.UserEntity.DisplayName,
|
UserDisplayName = makeCredentialsParams.UserEntity.DisplayName,
|
||||||
DiscoverableValue = makeCredentialsParams.RequireResidentKey,
|
DiscoverableValue = makeCredentialsParams.RequireResidentKey,
|
||||||
CreationDate = DateTime.UtcNow
|
CreationDate = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
@@ -316,12 +316,12 @@ namespace Bit.Core.Services
|
|||||||
);
|
);
|
||||||
authData.Add(flags);
|
authData.Add(flags);
|
||||||
|
|
||||||
authData.AddRange([
|
authData.AddRange(new List<byte> {
|
||||||
(byte)(counter >> 24),
|
(byte)(counter >> 24),
|
||||||
(byte)(counter >> 16),
|
(byte)(counter >> 16),
|
||||||
(byte)(counter >> 8),
|
(byte)(counter >> 8),
|
||||||
(byte)counter
|
(byte)counter
|
||||||
]);
|
});
|
||||||
|
|
||||||
if (isAttestation)
|
if (isAttestation)
|
||||||
{
|
{
|
||||||
|
|||||||
66
src/Core/Services/Logging/ClipLogger.cs
Normal file
66
src/Core/Services/Logging/ClipLogger.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
//using System.Runtime.CompilerServices;
|
||||||
|
//using System.Text;
|
||||||
|
//using Bit.Core.Abstractions;
|
||||||
|
|
||||||
|
//#if IOS
|
||||||
|
//using UIKit;
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
//namespace Bit.Core.Services
|
||||||
|
//{
|
||||||
|
// /// <summary>
|
||||||
|
// /// This logger can be used to help debug iOS extensions where we cannot use the .NET debugger yet
|
||||||
|
// /// so we can use this that copies the logs to the clipboard so one
|
||||||
|
// /// can paste them and analyze its output.
|
||||||
|
// /// </summary>
|
||||||
|
// public class ClipLogger : ILogger
|
||||||
|
// {
|
||||||
|
// private static readonly StringBuilder _currentBreadcrumbs = new StringBuilder();
|
||||||
|
|
||||||
|
// static ILogger _instance;
|
||||||
|
// public static ILogger Instance
|
||||||
|
// {
|
||||||
|
// get
|
||||||
|
// {
|
||||||
|
// if (_instance is null)
|
||||||
|
// {
|
||||||
|
// _instance = new ClipLogger();
|
||||||
|
// }
|
||||||
|
// return _instance;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// protected ClipLogger()
|
||||||
|
// {
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public static void Log(string breadcrumb)
|
||||||
|
// {
|
||||||
|
// _currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}");
|
||||||
|
//#if IOS
|
||||||
|
// UIPasteboard.General.String = _currentBreadcrumbs.ToString();
|
||||||
|
//#endif
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Error(string message, IDictionary<string, string> extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
|
||||||
|
// {
|
||||||
|
// var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}";
|
||||||
|
// var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}";
|
||||||
|
// var properties = new Dictionary<string, string>
|
||||||
|
// {
|
||||||
|
// ["File"] = filePathAndLineNumber,
|
||||||
|
// ["Method"] = memberName
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public void Exception(Exception ex) => Log(ex?.ToString());
|
||||||
|
|
||||||
|
// public Task InitAsync() => Task.CompletedTask;
|
||||||
|
|
||||||
|
// public Task<bool> IsEnabled() => Task.FromResult(true);
|
||||||
|
|
||||||
|
// public Task SetEnabled(bool value) => Task.CompletedTask;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
|
|||||||
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,16 +1,130 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AuthenticationServices;
|
using AuthenticationServices;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
using Bit.iOS.Core.Utilities;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
|
using Microsoft.Maui.ApplicationModel;
|
||||||
|
using ObjCRuntime;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill
|
namespace Bit.iOS.Autofill
|
||||||
{
|
{
|
||||||
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost
|
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost
|
||||||
{
|
{
|
||||||
|
private readonly LazyResolve<IFido2AuthenticatorService> _fido2AuthService = new LazyResolve<IFido2AuthenticatorService>();
|
||||||
|
|
||||||
|
public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest)
|
||||||
|
{
|
||||||
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (registrationRequest?.Type)
|
||||||
|
{
|
||||||
|
case ASCredentialRequestType.PasskeyAssertion:
|
||||||
|
var passkeyRegistrationRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(registrationRequest.GetHandle());
|
||||||
|
await PrepareInterfaceForPasskeyRegistrationAsync(passkeyRegistrationRequest);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
CancelRequest(ASExtensionErrorCode.Failed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnProvidingCredentialException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PrepareInterfaceForPasskeyRegistrationAsync(ASPasskeyCredentialRequest passkeyRegistrationRequest)
|
||||||
|
{
|
||||||
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0) || passkeyRegistrationRequest?.CredentialIdentity is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitAppIfNeeded();
|
||||||
|
|
||||||
|
if (!await IsAuthed())
|
||||||
|
{
|
||||||
|
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.PasskeyCredentialRequest = passkeyRegistrationRequest;
|
||||||
|
_context.IsCreatingPasskey = true;
|
||||||
|
|
||||||
|
var credIdentity = Runtime.GetNSObject<ASPasskeyCredentialIdentity>(passkeyRegistrationRequest.CredentialIdentity.GetHandle());
|
||||||
|
|
||||||
|
_context.UrlString = credIdentity?.RelyingPartyIdentifier;
|
||||||
|
|
||||||
|
var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
|
||||||
|
{
|
||||||
|
Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(),
|
||||||
|
CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms),
|
||||||
|
RequireUserVerification = passkeyRegistrationRequest.UserVerificationPreference == "required",
|
||||||
|
RequireResidentKey = true,
|
||||||
|
RpEntity = new PublicKeyCredentialRpEntity
|
||||||
|
{
|
||||||
|
Id = credIdentity.RelyingPartyIdentifier,
|
||||||
|
Name = credIdentity.RelyingPartyIdentifier
|
||||||
|
},
|
||||||
|
UserEntity = new PublicKeyCredentialUserEntity
|
||||||
|
{
|
||||||
|
Id = credIdentity.UserHandle.ToArray(),
|
||||||
|
Name = credIdentity.UserName,
|
||||||
|
DisplayName = credIdentity.UserName
|
||||||
|
}
|
||||||
|
}, new Fido2MakeCredentialUserInterface(EnsureUnlockedVaultAsync, _context, OnConfirmingNewCredential));
|
||||||
|
|
||||||
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
|
|
||||||
|
var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential(
|
||||||
|
credIdentity.RelyingPartyIdentifier,
|
||||||
|
passkeyRegistrationRequest.ClientDataHash,
|
||||||
|
NSData.FromArray(result.CredentialId),
|
||||||
|
NSData.FromArray(result.AttestationObject)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms)
|
||||||
|
{
|
||||||
|
if (supportedAlgorithms?.Any() != true)
|
||||||
|
{
|
||||||
|
return new PublicKeyCredentialParameters[]
|
||||||
|
{
|
||||||
|
new PublicKeyCredentialParameters
|
||||||
|
{
|
||||||
|
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
||||||
|
Alg = (int)Fido2AlgorithmIdentifier.ES256
|
||||||
|
},
|
||||||
|
new PublicKeyCredentialParameters
|
||||||
|
{
|
||||||
|
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
||||||
|
Alg = (int)Fido2AlgorithmIdentifier.RS256
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportedAlgorithms
|
||||||
|
.Where(alg => (int)alg == (int)Fido2AlgorithmIdentifier.ES256)
|
||||||
|
.Select(alg => new PublicKeyCredentialParameters
|
||||||
|
{
|
||||||
|
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
||||||
|
Alg = (int)alg
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
|
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
|
||||||
{
|
{
|
||||||
InitAppIfNeeded();
|
InitAppIfNeeded();
|
||||||
@@ -25,7 +139,7 @@ namespace Bit.iOS.Autofill
|
|||||||
await ProvideCredentialAsync(false);
|
await ProvideCredentialAsync(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CompleteAssertionRequestAsync(CipherView cipherView)
|
public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId)
|
||||||
{
|
{
|
||||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
{
|
{
|
||||||
@@ -33,44 +147,127 @@ namespace Bit.iOS.Autofill
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // TODO: Generate the credential Signature and Auth data accordingly
|
if (_context.PasskeyCredentialRequest is null)
|
||||||
// var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
|
{
|
||||||
// {
|
OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request without a PasskeyCredentialRequest"));
|
||||||
// RpId = cipherView.Login.MainFido2Credential.RpId,
|
return;
|
||||||
// Counter = cipherView.Login.MainFido2Credential.Counter,
|
|
||||||
// CredentialId = cipherView.Login.MainFido2Credential.CredentialId
|
|
||||||
// });
|
|
||||||
|
|
||||||
// CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
|
||||||
// cipherView.Login.MainFido2Credential.UserHandle,
|
|
||||||
// cipherView.Login.MainFido2Credential.RpId,
|
|
||||||
// NSData.FromArray(fido2AssertionResult.Signature),
|
|
||||||
// _context.PasskeyCredentialRequest?.ClientDataHash,
|
|
||||||
// NSData.FromArray(fido2AssertionResult.AuthenticatorData),
|
|
||||||
// cipherView.Login.MainFido2Credential.CredentialId
|
|
||||||
// ));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
|
try
|
||||||
{
|
{
|
||||||
if (_context == null)
|
// TODO: Add user verification and remove hardcoding on the user interface "userVerified"
|
||||||
|
var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
|
||||||
|
{
|
||||||
|
RpId = rpId,
|
||||||
|
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(),
|
||||||
|
RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required",
|
||||||
|
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
|
||||||
|
{
|
||||||
|
new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
|
||||||
|
{
|
||||||
|
Id = credentialIdData.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, new Fido2GetAssertionUserInterface(cipherId, true, EnsureUnlockedVaultAsync, () => Task.FromResult(true)));
|
||||||
|
|
||||||
|
var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null
|
||||||
|
? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle)
|
||||||
|
: (NSData)userHandleData;
|
||||||
|
|
||||||
|
var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null
|
||||||
|
? NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
||||||
|
: credentialIdData;
|
||||||
|
|
||||||
|
await CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
||||||
|
selectedUserHandleData,
|
||||||
|
rpId,
|
||||||
|
NSData.FromArray(fido2AssertionResult.Signature),
|
||||||
|
_context.PasskeyCredentialRequest.ClientDataHash,
|
||||||
|
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
|
||||||
|
selectedCredentialIdData
|
||||||
|
));
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (assertionCredential is null)
|
||||||
{
|
{
|
||||||
ServiceContainer.Reset();
|
ServiceContainer.Reset();
|
||||||
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSRunLoop.Main.BeginInvokeOnMainThread(() =>
|
|
||||||
{
|
|
||||||
ServiceContainer.Reset();
|
ServiceContainer.Reset();
|
||||||
ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null);
|
#pragma warning disable CA1416 // Validate platform compatibility
|
||||||
});
|
var expired = await ExtensionContext.CompleteAssertionRequestAsync(assertionCredential);
|
||||||
|
#pragma warning restore CA1416 // Validate platform compatibility
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView)
|
private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView)
|
||||||
{
|
{
|
||||||
return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials;
|
return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnConfirmingNewCredential()
|
||||||
|
{
|
||||||
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureUnlockedVaultAsync()
|
||||||
|
{
|
||||||
|
if (_context.IsCreatingPasskey)
|
||||||
|
{
|
||||||
|
if (!await IsLocked())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.UnlockVaultTcs?.SetCanceled();
|
||||||
|
_context.UnlockVaultTcs = new TaskCompletionSource<bool>();
|
||||||
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PerformSegue(SegueConstants.LOCK, this);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.UnlockVaultTcs.Task;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await IsAuthed() || await IsLocked())
|
||||||
|
{
|
||||||
|
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||||
|
throw new InvalidOperationException("Not authed or locked");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AuthenticationServices;
|
using AuthenticationServices;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
@@ -20,6 +19,7 @@ using Foundation;
|
|||||||
using Microsoft.Maui.ApplicationModel;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Platform;
|
using Microsoft.Maui.Platform;
|
||||||
|
using ObjCRuntime;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill
|
namespace Bit.iOS.Autofill
|
||||||
@@ -32,7 +32,6 @@ namespace Bit.iOS.Autofill
|
|||||||
private IAccountsManager _accountsManager;
|
private IAccountsManager _accountsManager;
|
||||||
|
|
||||||
private readonly LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
|
private readonly LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
|
||||||
private readonly LazyResolve<IFido2AuthenticationService> _fido2AuthService = new LazyResolve<IFido2AuthenticationService>();
|
|
||||||
|
|
||||||
public CredentialProviderViewController(IntPtr handle)
|
public CredentialProviderViewController(IntPtr handle)
|
||||||
: base(handle)
|
: base(handle)
|
||||||
@@ -56,7 +55,6 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
ExtContext = ExtensionContext
|
ExtContext = ExtensionContext
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -85,17 +83,17 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
else if (await IsLocked())
|
else if (await IsLocked())
|
||||||
{
|
{
|
||||||
PerformSegue("lockPasswordSegue", this);
|
PerformSegue(SegueConstants.LOCK, this);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
||||||
{
|
{
|
||||||
PerformSegue("loginSearchSegue", this);
|
PerformSegue(SegueConstants.LOGIN_SEARCH, this);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PerformSegue("loginListSegue", this);
|
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,17 +103,67 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest)
|
[Export("prepareCredentialListForServiceIdentifiers:requestParameters:")]
|
||||||
|
public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
switch (credentialRequest)
|
InitAppIfNeeded();
|
||||||
|
_context.ServiceIdentifiers = serviceIdentifiers;
|
||||||
|
if (serviceIdentifiers.Length > 0)
|
||||||
{
|
{
|
||||||
case ASPasswordCredentialRequest passwordRequest:
|
var uri = serviceIdentifiers[0].Identifier;
|
||||||
await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnProvidingCredentialException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Export("provideCredentialWithoutUserInteractionForRequest:")]
|
||||||
|
public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest)
|
||||||
|
{
|
||||||
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (credentialRequest?.Type)
|
||||||
|
{
|
||||||
|
case ASCredentialRequestType.Password:
|
||||||
|
var passwordCredentialIdentity = Runtime.GetNSObject<ASPasswordCredentialIdentity>(credentialRequest.CredentialIdentity.GetHandle());
|
||||||
|
await ProvideCredentialWithoutUserInteractionAsync(passwordCredentialIdentity);
|
||||||
break;
|
break;
|
||||||
case ASPasskeyCredentialRequest passkeyRequest:
|
case ASCredentialRequestType.PasskeyAssertion:
|
||||||
await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest);
|
var asPasskeyCredentialRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(credentialRequest.GetHandle());
|
||||||
|
await ProvideCredentialWithoutUserInteractionAsync(asPasskeyCredentialRequest);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
CancelRequest(ASExtensionErrorCode.Failed);
|
CancelRequest(ASExtensionErrorCode.Failed);
|
||||||
@@ -128,11 +176,73 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
//public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
||||||
|
//{
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity);
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// OnProvidingCredentialException(ex);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
[Export("prepareInterfaceToProvideCredentialForRequest:")]
|
||||||
|
public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest)
|
||||||
|
{
|
||||||
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (credentialRequest?.Type)
|
||||||
|
{
|
||||||
|
case ASCredentialRequestType.Password:
|
||||||
|
var passwordCredentialIdentity = Runtime.GetNSObject<ASPasswordCredentialIdentity>(credentialRequest.CredentialIdentity.GetHandle());
|
||||||
|
await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = passwordCredentialIdentity);
|
||||||
|
break;
|
||||||
|
case ASCredentialRequestType.PasskeyAssertion:
|
||||||
|
var asPasskeyCredentialRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(credentialRequest.GetHandle());
|
||||||
|
await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = asPasskeyCredentialRequest);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
CancelRequest(ASExtensionErrorCode.Failed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnProvidingCredentialException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
|
||||||
|
//{
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity);
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// OnProvidingCredentialException(ex);
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
public override async void PrepareInterfaceForExtensionConfiguration()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity);
|
InitAppIfNeeded();
|
||||||
|
_context.Configuring = true;
|
||||||
|
if (!await IsAuthed())
|
||||||
|
{
|
||||||
|
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await CheckLockAsync(() => PerformSegue("setupSegue", this));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -156,41 +266,6 @@ namespace Bit.iOS.Autofill
|
|||||||
await ProvideCredentialAsync(false);
|
await ProvideCredentialAsync(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
switch (credentialRequest)
|
|
||||||
{
|
|
||||||
case ASPasswordCredentialRequest passwordRequest:
|
|
||||||
PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
|
||||||
break;
|
|
||||||
case ASPasskeyCredentialRequest passkeyRequest:
|
|
||||||
await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = passkeyRequest);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
OnProvidingCredentialException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
OnProvidingCredentialException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PrepareInterfaceToProvideCredentialAsync(Action<Context> updateContext)
|
private async Task PrepareInterfaceToProvideCredentialAsync(Action<Context> updateContext)
|
||||||
{
|
{
|
||||||
InitAppIfNeeded();
|
InitAppIfNeeded();
|
||||||
@@ -203,26 +278,6 @@ namespace Bit.iOS.Autofill
|
|||||||
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override async void PrepareInterfaceForExtensionConfiguration()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InitAppIfNeeded();
|
|
||||||
_context.Configuring = true;
|
|
||||||
if (!await IsAuthed())
|
|
||||||
{
|
|
||||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await CheckLockAsync(() => PerformSegue("setupSegue", this));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
OnProvidingCredentialException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CompleteRequest(string id = null, string username = null,
|
public void CompleteRequest(string id = null, string username = null,
|
||||||
string password = null, string totp = null)
|
string password = null, string totp = null)
|
||||||
{
|
{
|
||||||
@@ -262,13 +317,18 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
private void OnProvidingCredentialException(Exception ex)
|
private void OnProvidingCredentialException(Exception ex)
|
||||||
{
|
{
|
||||||
//LoggerHelper.LogEvenIfCantBeResolved(ex);
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
UIPasteboard.General.String = ex.ToString();
|
|
||||||
CancelRequest(ASExtensionErrorCode.Failed);
|
CancelRequest(ASExtensionErrorCode.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancelRequest(ASExtensionErrorCode code)
|
public void CancelRequest(ASExtensionErrorCode 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(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null);
|
||||||
var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code);
|
var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code);
|
||||||
ExtensionContext?.CancelRequest(err);
|
ExtensionContext?.CancelRequest(err);
|
||||||
@@ -316,7 +376,7 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void DismissLockAndContinue()
|
public void DismissLockAndContinue()
|
||||||
{
|
{
|
||||||
DismissViewController(false, async () => await OnLockDismissedAsync());
|
DismissViewController(false, async () => await OnLockDismissedAsync());
|
||||||
}
|
}
|
||||||
@@ -334,7 +394,13 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_context.PasswordCredentialIdentity != null)
|
if (_context.IsCreatingPasskey)
|
||||||
|
{
|
||||||
|
_context.UnlockVaultTcs.SetResult(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_context.PasswordCredentialIdentity != null || _context.IsPasskey)
|
||||||
{
|
{
|
||||||
await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync());
|
await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync());
|
||||||
return;
|
return;
|
||||||
@@ -364,6 +430,20 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (_context.IsPasskey && UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
if (_context.PasskeyCredentialIdentity is null)
|
||||||
|
{
|
||||||
|
CancelRequest(ASExtensionErrorCode.Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
await CompleteAssertionRequestAsync(_context.PasskeyCredentialIdentity.RelyingPartyIdentifier,
|
||||||
|
_context.PasskeyCredentialIdentity.UserHandle,
|
||||||
|
_context.PasskeyCredentialIdentity.CredentialId,
|
||||||
|
_context.RecordIdentifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ServiceContainer.TryResolve<ICipherService>(out var cipherService)
|
if (!ServiceContainer.TryResolve<ICipherService>(out var cipherService)
|
||||||
||
|
||
|
||||||
_context.RecordIdentifier == null)
|
_context.RecordIdentifier == null)
|
||||||
@@ -411,12 +491,6 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_context.IsPasskey)
|
|
||||||
{
|
|
||||||
await CompleteAssertionRequestAsync(decCipher);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string totpCode = null;
|
string totpCode = null;
|
||||||
if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true)
|
if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true)
|
||||||
{
|
{
|
||||||
@@ -702,11 +776,11 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.Lock:
|
case NavigationTarget.Lock:
|
||||||
DismissViewController(false, () => PerformSegue("lockPasswordSegue", this));
|
DismissViewController(false, () => PerformSegue(SegueConstants.LOCK, this));
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.AutofillCiphers:
|
case NavigationTarget.AutofillCiphers:
|
||||||
case NavigationTarget.Home:
|
case NavigationTarget.Home:
|
||||||
DismissViewController(false, () => PerformSegue("loginListSegue", this));
|
DismissViewController(false, () => PerformSegue(SegueConstants.LOGIN_LIST, this));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs
Normal file
36
src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.iOS.Autofill.Models;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Autofill
|
||||||
|
{
|
||||||
|
public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialUserInterface
|
||||||
|
{
|
||||||
|
private readonly Func<Task> _ensureUnlockedVaultCallback;
|
||||||
|
private readonly Context _context;
|
||||||
|
private readonly Action _onConfirmingNewCredential;
|
||||||
|
|
||||||
|
public Fido2MakeCredentialUserInterface(Func<Task> ensureUnlockedVaultCallback, Context context, Action onConfirmingNewCredential)
|
||||||
|
{
|
||||||
|
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||||
|
_context = context;
|
||||||
|
_onConfirmingNewCredential = onConfirmingNewCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
|
||||||
|
{
|
||||||
|
_context.ConfirmNewCredentialTcs?.SetCanceled();
|
||||||
|
_context.ConfirmNewCredentialTcs = new TaskCompletionSource<(string CipherId, bool UserVerified)>();
|
||||||
|
|
||||||
|
_onConfirmingNewCredential();
|
||||||
|
|
||||||
|
return await _context.ConfirmNewCredentialTcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS doesn't seem to provide the ExcludeCredentialDescriptorList so nothing to do here currently.
|
||||||
|
public Task InformExcludedCredentialAsync(string[] existingCipherIds) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task EnsureUnlockedVaultAsync() => _ensureUnlockedVaultCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
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 System;
|
||||||
using Bit.App.Abstractions;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.iOS.Autofill.ListItems;
|
||||||
using Bit.iOS.Autofill.Models;
|
using Bit.iOS.Autofill.Models;
|
||||||
using Bit.iOS.Autofill.Utilities;
|
using Bit.iOS.Autofill.Utilities;
|
||||||
using Bit.iOS.Core.Controllers;
|
using Bit.iOS.Core.Controllers;
|
||||||
using Bit.iOS.Core.Utilities;
|
using Bit.iOS.Core.Utilities;
|
||||||
using Bit.iOS.Core.Views;
|
using Bit.iOS.Core.Views;
|
||||||
using CoreFoundation;
|
using CoreFoundation;
|
||||||
|
using CoreGraphics;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill
|
namespace Bit.iOS.Autofill
|
||||||
{
|
{
|
||||||
public partial class LoginListViewController : ExtendedUIViewController
|
public partial class LoginListViewController : ExtendedUIViewController, ILoginListViewController
|
||||||
{
|
{
|
||||||
|
internal const string HEADER_SECTION_IDENTIFIER = "headerSectionId";
|
||||||
|
|
||||||
UIBarButtonItem _cancelButton;
|
UIBarButtonItem _cancelButton;
|
||||||
UIControl _accountSwitchButton;
|
UIControl _accountSwitchButton;
|
||||||
|
|
||||||
@@ -24,21 +30,24 @@ namespace Bit.iOS.Autofill
|
|||||||
: base(handle)
|
: base(handle)
|
||||||
{
|
{
|
||||||
DismissModalAction = Cancel;
|
DismissModalAction = Cancel;
|
||||||
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context Context { get; set; }
|
public Context Context { get; set; }
|
||||||
public CredentialProviderViewController CPViewController { get; set; }
|
public CredentialProviderViewController CPViewController { get; set; }
|
||||||
public IPasswordRepromptService PasswordRepromptService { get; private set; }
|
|
||||||
|
|
||||||
AccountSwitchingOverlayView _accountSwitchingOverlayView;
|
AccountSwitchingOverlayView _accountSwitchingOverlayView;
|
||||||
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
|
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
|
||||||
|
|
||||||
LazyResolve<IBroadcasterService> _broadcasterService = new LazyResolve<IBroadcasterService>("broadcasterService");
|
LazyResolve<IBroadcasterService> _broadcasterService = new LazyResolve<IBroadcasterService>();
|
||||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||||
|
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||||
|
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||||
|
|
||||||
bool _alreadyLoadItemsOnce = false;
|
bool _alreadyLoadItemsOnce = false;
|
||||||
|
|
||||||
public async override void ViewDidLoad()
|
public async override void ViewDidLoad()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
||||||
|
|
||||||
@@ -46,14 +55,39 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
SubscribeSyncCompleted();
|
SubscribeSyncCompleted();
|
||||||
|
|
||||||
NavItem.Title = AppResources.Items;
|
NavItem.Title = Context.IsCreatingPasskey ? AppResources.SavePasskey : AppResources.Items;
|
||||||
_cancelButton.Title = AppResources.Cancel;
|
_cancelButton.Title = AppResources.Cancel;
|
||||||
|
|
||||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||||
TableView.EstimatedRowHeight = 44;
|
TableView.EstimatedRowHeight = 44;
|
||||||
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||||
TableView.Source = new TableSource(this);
|
TableView.Source = new TableSource(this);
|
||||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
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;
|
_alreadyLoadItemsOnce = true;
|
||||||
|
|
||||||
@@ -78,6 +112,11 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelButton_TouchUpInside(object sender, EventArgs e)
|
private void CancelButton_TouchUpInside(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
@@ -91,17 +130,37 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
private void Cancel()
|
private void Cancel()
|
||||||
{
|
{
|
||||||
CPViewController.CompleteRequest();
|
CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
||||||
{
|
{
|
||||||
PerformSegue("loginAddSegue", this);
|
PerformSegue(SegueConstants.ADD_LOGIN, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void SearchBarButton_Activated(UIBarButtonItem sender)
|
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((cipherId, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||||
@@ -136,7 +195,7 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
await ((TableSource)TableView.Source).LoadAsync();
|
||||||
TableView.ReloadData();
|
TableView.ReloadData();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -148,6 +207,13 @@ namespace Bit.iOS.Autofill
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnEmptyList()
|
||||||
|
{
|
||||||
|
_emptyView.Hidden = false;
|
||||||
|
_headerView.Hidden = false;
|
||||||
|
TableView.Hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
public override void ViewDidUnload()
|
public override void ViewDidUnload()
|
||||||
{
|
{
|
||||||
base.ViewDidUnload();
|
base.ViewDidUnload();
|
||||||
@@ -159,8 +225,15 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
DismissViewController(true, async () =>
|
DismissViewController(true, async () =>
|
||||||
{
|
{
|
||||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
try
|
||||||
|
{
|
||||||
|
await ((TableSource)TableView.Source).LoadAsync();
|
||||||
TableView.ReloadData();
|
TableView.ReloadData();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Value.Exception(ex);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,20 +252,61 @@ namespace Bit.iOS.Autofill
|
|||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TableSource : ExtensionTableSource
|
public class TableSource : BaseLoginListTableSource<LoginListViewController>
|
||||||
{
|
{
|
||||||
private LoginListViewController _controller;
|
|
||||||
|
|
||||||
public TableSource(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,
|
try
|
||||||
_controller.CPViewController, _controller, _controller.PasswordRepromptService, "loginAddSegue");
|
{
|
||||||
|
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")]
|
[Register ("LoginListViewController")]
|
||||||
partial class 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]
|
[Outlet]
|
||||||
[GeneratedCode ("iOS Designer", "1.0")]
|
[GeneratedCode ("iOS Designer", "1.0")]
|
||||||
UIKit.UIBarButtonItem AddBarButton { get; set; }
|
UIKit.UIBarButtonItem AddBarButton { get; set; }
|
||||||
@@ -32,11 +50,44 @@ namespace Bit.iOS.Autofill
|
|||||||
[Action ("AddBarButton_Activated:")]
|
[Action ("AddBarButton_Activated:")]
|
||||||
partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender);
|
partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender);
|
||||||
|
|
||||||
|
[Action ("EmptyButton_Activated:")]
|
||||||
|
partial void EmptyButton_Activated (UIKit.UIButton sender);
|
||||||
|
|
||||||
[Action ("SearchBarButton_Activated:")]
|
[Action ("SearchBarButton_Activated:")]
|
||||||
partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender);
|
partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender);
|
||||||
|
|
||||||
void ReleaseDesignerOutlets ()
|
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) {
|
if (AddBarButton != null) {
|
||||||
AddBarButton.Dispose ();
|
AddBarButton.Dispose ();
|
||||||
AddBarButton = null;
|
AddBarButton = null;
|
||||||
|
|||||||
@@ -12,19 +12,17 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.iOS.Autofill
|
namespace Bit.iOS.Autofill
|
||||||
{
|
{
|
||||||
public partial class LoginSearchViewController : ExtendedUITableViewController
|
public partial class LoginSearchViewController : ExtendedUITableViewController, ILoginListViewController
|
||||||
{
|
{
|
||||||
public LoginSearchViewController(IntPtr handle)
|
public LoginSearchViewController(IntPtr handle)
|
||||||
: base(handle)
|
: base(handle)
|
||||||
{
|
{
|
||||||
DismissModalAction = Cancel;
|
DismissModalAction = Cancel;
|
||||||
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context Context { get; set; }
|
public Context Context { get; set; }
|
||||||
public CredentialProviderViewController CPViewController { get; set; }
|
public CredentialProviderViewController CPViewController { get; set; }
|
||||||
public bool FromList { get; set; }
|
public bool FromList { get; set; }
|
||||||
public IPasswordRepromptService PasswordRepromptService { get; private set; }
|
|
||||||
|
|
||||||
public async override void ViewDidLoad()
|
public async override void ViewDidLoad()
|
||||||
{
|
{
|
||||||
@@ -39,7 +37,7 @@ namespace Bit.iOS.Autofill
|
|||||||
TableView.EstimatedRowHeight = 44;
|
TableView.EstimatedRowHeight = 44;
|
||||||
TableView.Source = new TableSource(this);
|
TableView.Source = new TableSource(this);
|
||||||
SearchBar.Delegate = new ExtensionSearchDelegate(TableView);
|
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)
|
public override void ViewDidAppear(bool animated)
|
||||||
@@ -61,13 +59,13 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CPViewController.CompleteRequest();
|
CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
||||||
{
|
{
|
||||||
PerformSegue("loginAddFromSearchSegue", this);
|
PerformSegue(SegueConstants.ADD_LOGIN_FROM_SEARCH, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||||
@@ -88,29 +86,19 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
DismissViewController(true, async () =>
|
DismissViewController(true, async () =>
|
||||||
{
|
{
|
||||||
await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text);
|
await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
||||||
TableView.ReloadData();
|
TableView.ReloadData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TableSource : ExtensionTableSource
|
public class TableSource : BaseLoginListTableSource<LoginSearchViewController>
|
||||||
{
|
{
|
||||||
private Context _context;
|
|
||||||
private LoginSearchViewController _controller;
|
|
||||||
|
|
||||||
public TableSource(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)
|
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN_FROM_SEARCH;
|
||||||
{
|
|
||||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
|
||||||
_controller.CPViewController, _controller, _controller.PasswordRepromptService,
|
|
||||||
"loginAddFromSearchSegue");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
<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="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.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"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="830"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="781"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<prototypes>
|
<prototypes>
|
||||||
@@ -173,13 +242,20 @@
|
|||||||
<viewLayoutGuide key="safeArea" id="BQW-dG-XMM"/>
|
<viewLayoutGuide key="safeArea" id="BQW-dG-XMM"/>
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
<constraints>
|
<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="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="trailing" secondItem="Tq0-Ep-tHr" secondAttribute="trailing" id="5BV-0y-vU1"/>
|
||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="2305" secondAttribute="bottom" id="6EB-rh-lLS"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="2305" secondAttribute="bottom" id="6EB-rh-lLS"/>
|
||||||
|
<constraint firstItem="wNm-Sy-bJv" firstAttribute="top" secondItem="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="Tq0-Ep-tHr" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="eT6-Bv-JaR"/>
|
||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="2305" secondAttribute="trailing" id="ofJ-fL-adF"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="2305" secondAttribute="trailing" id="ofJ-fL-adF"/>
|
||||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="Tq0-Ep-tHr" secondAttribute="bottom" id="pBa-o1-Mtx"/>
|
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="Tq0-Ep-tHr" secondAttribute="bottom" id="pBa-o1-Mtx"/>
|
||||||
<constraint firstItem="2305" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="pGe-1e-B4s"/>
|
<constraint firstItem="2305" firstAttribute="top" secondItem="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"/>
|
<constraint firstItem="2305" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="xfQ-VQ-yWe"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
@@ -207,6 +283,12 @@
|
|||||||
<outlet property="NavItem" destination="3734" id="name-outlet-3734"/>
|
<outlet property="NavItem" destination="3734" id="name-outlet-3734"/>
|
||||||
<outlet property="OverlayView" destination="Tq0-Ep-tHr" id="igj-R2-gXJ"/>
|
<outlet property="OverlayView" destination="Tq0-Ep-tHr" id="igj-R2-gXJ"/>
|
||||||
<outlet property="TableView" destination="2305" id="aUe-Uz-iIb"/>
|
<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="1845" kind="presentation" identifier="loginAddSegue" modalPresentationStyle="fullScreen" modalTransitionStyle="coverVertical" id="3731"/>
|
||||||
<segue destination="11552" kind="show" identifier="loginSearchFromListSegue" id="12574"/>
|
<segue destination="11552" kind="show" identifier="loginSearchFromListSegue" id="12574"/>
|
||||||
</connections>
|
</connections>
|
||||||
@@ -575,7 +657,14 @@
|
|||||||
</inferredMetricsTieBreakers>
|
</inferredMetricsTieBreakers>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="check.png" width="90" height="90"/>
|
<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"/>
|
<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">
|
<systemColor name="darkTextColor">
|
||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
</systemColor>
|
</systemColor>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using AuthenticationServices;
|
using System.Threading.Tasks;
|
||||||
|
using AuthenticationServices;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.iOS.Core.Models;
|
using Bit.iOS.Core.Models;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
|
using ObjCRuntime;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill.Models
|
namespace Bit.iOS.Autofill.Models
|
||||||
@@ -12,14 +15,17 @@ namespace Bit.iOS.Autofill.Models
|
|||||||
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
||||||
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
||||||
public bool Configuring { get; set; }
|
public bool Configuring { get; set; }
|
||||||
|
public bool IsCreatingPasskey { get; set; }
|
||||||
|
public TaskCompletionSource<bool> UnlockVaultTcs { get; set; }
|
||||||
|
public TaskCompletionSource<(string CipherId, bool UserVerified)> ConfirmNewCredentialTcs { get; set; }
|
||||||
|
|
||||||
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
if (PasskeyCredentialRequest != null && UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
{
|
{
|
||||||
return PasskeyCredentialRequest?.CredentialIdentity as ASPasskeyCredentialIdentity;
|
return Runtime.GetNSObject<ASPasskeyCredentialIdentity>(PasskeyCredentialRequest.CredentialIdentity.GetHandle());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
Normal file
87
src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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((item.Id, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<ApplicationId>com.8bit.bitwarden.autofill</ApplicationId>
|
<ApplicationId>com.8bit.bitwarden.autofill</ApplicationId>
|
||||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||||
<ApplicationVersion>1</ApplicationVersion>
|
<ApplicationVersion>1</ApplicationVersion>
|
||||||
|
<RootNamespace>Bit.iOS.Autofill</RootNamespace>
|
||||||
|
|
||||||
<EnableDefaultCompileItems>False</EnableDefaultCompileItems>
|
<EnableDefaultCompileItems>False</EnableDefaultCompileItems>
|
||||||
|
|
||||||
@@ -44,6 +45,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<TrimmerRootAssembly Include="System.Security.Cryptography" />
|
<TrimmerRootAssembly Include="System.Security.Cryptography" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="ListItems\" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="CredentialProviderViewController.cs" />
|
<Compile Include="CredentialProviderViewController.cs" />
|
||||||
<Compile Include="CredentialProviderViewController.designer.cs">
|
<Compile Include="CredentialProviderViewController.designer.cs">
|
||||||
@@ -82,6 +86,12 @@
|
|||||||
<BundleResource Include="Resources\MaterialIcons_Regular.ttf" />
|
<BundleResource Include="Resources\MaterialIcons_Regular.ttf" />
|
||||||
<BundleResource Include="Resources\bwi-font.ttf" />
|
<BundleResource Include="Resources\bwi-font.ttf" />
|
||||||
<Compile Include="CredentialProviderViewController.Passkeys.cs" />
|
<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" />
|
||||||
|
<Compile Include="Fido2MakeCredentialUserInterface.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<BundleResource Include="Resources\check.png" />
|
<BundleResource Include="Resources\check.png" />
|
||||||
@@ -180,4 +190,7 @@
|
|||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
<ProjectReference Include="..\iOS.Core\iOS.Core.csproj" />
|
<ProjectReference Include="..\iOS.Core\iOS.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="ListItems\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Bit.Core.Abstractions;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Core.Utilities
|
namespace Bit.iOS.Core.Utilities
|
||||||
@@ -146,7 +147,7 @@ namespace Bit.iOS.Core.Utilities
|
|||||||
|
|
||||||
return new ASPasskeyCredentialIdentity(cipher.Login.MainFido2Credential.RpId,
|
return new ASPasskeyCredentialIdentity(cipher.Login.MainFido2Credential.RpId,
|
||||||
cipher.Login.MainFido2Credential.UserName,
|
cipher.Login.MainFido2Credential.UserName,
|
||||||
cipher.Login.MainFido2Credential.CredentialId,
|
NSData.FromArray(cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat()),
|
||||||
cipher.Login.MainFido2Credential.UserHandle,
|
cipher.Login.MainFido2Credential.UserHandle,
|
||||||
cipher.Id);
|
cipher.Id);
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/iOS.Core/Utilities/NSDataExtensions.cs
Normal file
15
src/iOS.Core/Utilities/NSDataExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Foundation;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Utilities
|
||||||
|
{
|
||||||
|
public static class NSDataExtensions
|
||||||
|
{
|
||||||
|
public static byte[] ToByteArray(this NSData data)
|
||||||
|
{
|
||||||
|
var bytes = new byte[data.Length];
|
||||||
|
Marshal.Copy(data.Bytes, bytes, 0, Convert.ToInt32(data.Length));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,7 @@ namespace Bit.iOS.Core.Utilities
|
|||||||
logger!.Exception(nreAppGroupContainer);
|
logger!.Exception(nreAppGroupContainer);
|
||||||
throw nreAppGroupContainer;
|
throw nreAppGroupContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
var liteDbStorage = new LiteDbStorageService(
|
var liteDbStorage = new LiteDbStorageService(
|
||||||
Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db"));
|
Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db"));
|
||||||
var localizeService = new LocalizeService();
|
var localizeService = new LocalizeService();
|
||||||
@@ -189,6 +190,11 @@ namespace Bit.iOS.Core.Utilities
|
|||||||
|
|
||||||
public static void RegisterFinallyBeforeBootstrap()
|
public static void RegisterFinallyBeforeBootstrap()
|
||||||
{
|
{
|
||||||
|
ServiceContainer.Register<IFido2AuthenticatorService>(new Fido2AuthenticatorService(
|
||||||
|
ServiceContainer.Resolve<ICipherService>(),
|
||||||
|
ServiceContainer.Resolve<ISyncService>(),
|
||||||
|
ServiceContainer.Resolve<ICryptoFunctionService>()));
|
||||||
|
|
||||||
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
|
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
|
||||||
ServiceContainer.Resolve<IEnvironmentService>(),
|
ServiceContainer.Resolve<IEnvironmentService>(),
|
||||||
ServiceContainer.Resolve<IStateService>(),
|
ServiceContainer.Resolve<IStateService>(),
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Resources.Localization;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.iOS.Core.Controllers;
|
using Bit.iOS.Core.Controllers;
|
||||||
using Bit.iOS.Core.Models;
|
using Bit.iOS.Core.Models;
|
||||||
@@ -25,8 +20,8 @@ namespace Bit.iOS.Core.Views
|
|||||||
protected ITotpService _totpService;
|
protected ITotpService _totpService;
|
||||||
protected IStateService _stateService;
|
protected IStateService _stateService;
|
||||||
protected ISearchService _searchService;
|
protected ISearchService _searchService;
|
||||||
private AppExtensionContext _context;
|
protected AppExtensionContext _context;
|
||||||
private UIViewController _controller;
|
protected UIViewController _controller;
|
||||||
|
|
||||||
public ExtensionTableSource(AppExtensionContext context, UIViewController controller)
|
public ExtensionTableSource(AppExtensionContext context, UIViewController controller)
|
||||||
{
|
{
|
||||||
@@ -36,11 +31,19 @@ namespace Bit.iOS.Core.Views
|
|||||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||||
_context = context;
|
_context = context;
|
||||||
_controller = controller;
|
_controller = controller;
|
||||||
|
|
||||||
|
Items = new List<CipherViewModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<CipherViewModel> Items { get; private set; }
|
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>();
|
var combinedLogins = new List<CipherView>();
|
||||||
|
|
||||||
@@ -62,11 +65,10 @@ namespace Bit.iOS.Core.Views
|
|||||||
combinedLogins.AddRange(logins);
|
combinedLogins.AddRange(logins);
|
||||||
}
|
}
|
||||||
|
|
||||||
_allItems = combinedLogins
|
return combinedLogins
|
||||||
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
|
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
|
||||||
.Select(s => new CipherViewModel(s))
|
.Select(s => new CipherViewModel(s))
|
||||||
.ToList() ?? new List<CipherViewModel>();
|
.ToList();
|
||||||
FilterResults(searchFilter, new CancellationToken());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FilterResults(string searchFilter, CancellationToken ct)
|
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)
|
public override nint RowsInSection(UITableView tableview, nint section)
|
||||||
{
|
{
|
||||||
@@ -135,9 +137,9 @@ namespace Bit.iOS.Core.Views
|
|||||||
cell.DetailTextLabel.Text = item.Username;
|
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();
|
var accessPremium = await _stateService.CanAccessPremiumAsync();
|
||||||
if (accessPremium || (item?.CipherView.OrganizationUseTotp ?? false))
|
if (accessPremium || (item?.CipherView.OrganizationUseTotp ?? false))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace Bit.iOS.Extension
|
|||||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||||
TableView.EstimatedRowHeight = 44;
|
TableView.EstimatedRowHeight = 44;
|
||||||
TableView.Source = new TableSource(this);
|
TableView.Source = new TableSource(this);
|
||||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
await ((TableSource)TableView.Source).LoadAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAutoFill()
|
public bool CanAutoFill()
|
||||||
@@ -93,7 +93,7 @@ namespace Bit.iOS.Extension
|
|||||||
{
|
{
|
||||||
DismissViewController(true, async () =>
|
DismissViewController(true, async () =>
|
||||||
{
|
{
|
||||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
await ((TableSource)TableView.Source).LoadAsync();
|
||||||
TableView.ReloadData();
|
TableView.ReloadData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user