mirror of
https://github.com/bitwarden/mobile.git
synced 2025-02-15 01:01:25 +01:00
[PM-1129] iOS 16 Third-Party 2FA OTP handling (#2409)
* [EC-980] Added iOS otpauth handler (#2370) * EC-980 added Bitwarden as otpauth scheme handler * EC-980 Fix format * [EC-981] OTP handling - Set to selected cipher (#2404) * EC-981 Started adding OTP to existing cipher. Reused AutofillCiphersPage for the cipher selection and refactored it so that we have more code reuse * EC-981 Fix navigation on otp handling * EC-981 Fix formatting * EC-981 Added otp cipher selection callout and add close toolbar item when needed * PM-1131 implemented cipher creation from otp handling flow with otp key filled (#2407) * PM-1133 Updated empty states for search and cipher selection on otp flow (#2408)
This commit is contained in:
parent
4d2b53c809
commit
a18f74a72a
@ -170,7 +170,7 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
if (intent?.GetStringExtra("uri") is string uri)
|
if (intent?.GetStringExtra("uri") is string uri)
|
||||||
{
|
{
|
||||||
_messagingService.Send("popAllAndGoToAutofillCiphers");
|
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE);
|
||||||
if (_appOptions != null)
|
if (_appOptions != null)
|
||||||
{
|
{
|
||||||
_appOptions.Uri = uri;
|
_appOptions.Uri = uri;
|
||||||
@ -178,7 +178,7 @@ namespace Bit.Droid
|
|||||||
}
|
}
|
||||||
else if (intent.GetBooleanExtra("generatorTile", false))
|
else if (intent.GetBooleanExtra("generatorTile", false))
|
||||||
{
|
{
|
||||||
_messagingService.Send("popAllAndGoToTabGenerator");
|
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE);
|
||||||
if (_appOptions != null)
|
if (_appOptions != null)
|
||||||
{
|
{
|
||||||
_appOptions.GeneratorTile = true;
|
_appOptions.GeneratorTile = true;
|
||||||
@ -186,7 +186,7 @@ namespace Bit.Droid
|
|||||||
}
|
}
|
||||||
else if (intent.GetBooleanExtra("myVaultTile", false))
|
else if (intent.GetBooleanExtra("myVaultTile", false))
|
||||||
{
|
{
|
||||||
_messagingService.Send("popAllAndGoToTabMyVault");
|
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE);
|
||||||
if (_appOptions != null)
|
if (_appOptions != null)
|
||||||
{
|
{
|
||||||
_appOptions.MyVaultTile = true;
|
_appOptions.MyVaultTile = true;
|
||||||
@ -198,7 +198,7 @@ namespace Bit.Droid
|
|||||||
{
|
{
|
||||||
_appOptions.CreateSend = GetCreateSendRequest(intent);
|
_appOptions.CreateSend = GetCreateSendRequest(intent);
|
||||||
}
|
}
|
||||||
_messagingService.Send("popAllAndGoToTabSend");
|
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@ namespace Bit.App.Abstractions
|
|||||||
{
|
{
|
||||||
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
|
||||||
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
|
||||||
|
Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction);
|
||||||
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
Task LogOutAsync(string userId, bool userInitiated, bool expired);
|
||||||
Task PromptToSwitchToExistingAccountAsync(string userId);
|
Task PromptToSwitchToExistingAccountAsync(string userId);
|
||||||
}
|
}
|
||||||
|
9
src/App/Abstractions/IDeepLinkContext.cs
Normal file
9
src/App/Abstractions/IDeepLinkContext.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Bit.App.Abstractions
|
||||||
|
{
|
||||||
|
public interface IDeepLinkContext
|
||||||
|
{
|
||||||
|
bool OnNewUri(Uri uri);
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,11 @@ namespace Bit.App
|
|||||||
{
|
{
|
||||||
public partial class App : Application, IAccountsManagerHost
|
public partial class App : Application, IAccountsManagerHost
|
||||||
{
|
{
|
||||||
|
public const string POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE = "popAllAndGoToTabGenerator";
|
||||||
|
public const string POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE = "popAllAndGoToTabMyVault";
|
||||||
|
public const string POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE = "popAllAndGoToTabSend";
|
||||||
|
public const string POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE = "popAllAndGoToAutofillCiphers";
|
||||||
|
|
||||||
private readonly IBroadcasterService _broadcasterService;
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
@ -103,12 +108,18 @@ namespace Bit.App
|
|||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
await _accountsManager.NavigateOnAccountChangeAsync();
|
await _accountsManager.NavigateOnAccountChangeAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == "popAllAndGoToTabGenerator" ||
|
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE ||
|
||||||
message.Command == "popAllAndGoToTabMyVault" ||
|
message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
|
||||||
message.Command == "popAllAndGoToTabSend" ||
|
message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
|
||||||
message.Command == "popAllAndGoToAutofillCiphers")
|
message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
|
||||||
|
message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||||
{
|
{
|
||||||
Device.BeginInvokeOnMainThread(async () =>
|
if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||||
|
{
|
||||||
|
Options.OtpData = new OtpData((string)message.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Device.InvokeOnMainThreadAsync(async () =>
|
||||||
{
|
{
|
||||||
if (Current.MainPage is TabsPage tabsPage)
|
if (Current.MainPage is TabsPage tabsPage)
|
||||||
{
|
{
|
||||||
@ -116,24 +127,29 @@ namespace Bit.App
|
|||||||
{
|
{
|
||||||
await tabsPage.Navigation.PopModalAsync(false);
|
await tabsPage.Navigation.PopModalAsync(false);
|
||||||
}
|
}
|
||||||
if (message.Command == "popAllAndGoToAutofillCiphers")
|
if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
|
||||||
{
|
{
|
||||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
Current.MainPage = new NavigationPage(new CipherSelectionPage(Options));
|
||||||
}
|
}
|
||||||
else if (message.Command == "popAllAndGoToTabMyVault")
|
else if (message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE)
|
||||||
{
|
{
|
||||||
Options.MyVaultTile = false;
|
Options.MyVaultTile = false;
|
||||||
tabsPage.ResetToVaultPage();
|
tabsPage.ResetToVaultPage();
|
||||||
}
|
}
|
||||||
else if (message.Command == "popAllAndGoToTabGenerator")
|
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE)
|
||||||
{
|
{
|
||||||
Options.GeneratorTile = false;
|
Options.GeneratorTile = false;
|
||||||
tabsPage.ResetToGeneratorPage();
|
tabsPage.ResetToGeneratorPage();
|
||||||
}
|
}
|
||||||
else if (message.Command == "popAllAndGoToTabSend")
|
else if (message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE)
|
||||||
{
|
{
|
||||||
tabsPage.ResetToSendPage();
|
tabsPage.ResetToSendPage();
|
||||||
}
|
}
|
||||||
|
else if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
|
||||||
|
{
|
||||||
|
tabsPage.ResetToVaultPage();
|
||||||
|
await tabsPage.Navigation.PushModalAsync(new NavigationPage(new CipherSelectionPage(Options)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -494,7 +510,8 @@ namespace Bit.App
|
|||||||
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
|
Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: Options));
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.AutofillCiphers:
|
case NavigationTarget.AutofillCiphers:
|
||||||
Current.MainPage = new NavigationPage(new AutofillCiphersPage(Options));
|
case NavigationTarget.OtpCipherSelection:
|
||||||
|
Current.MainPage = new NavigationPage(new CipherSelectionPage(Options));
|
||||||
break;
|
break;
|
||||||
case NavigationTarget.SendAddEdit:
|
case NavigationTarget.SendAddEdit:
|
||||||
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
|
Current.MainPage = new NavigationPage(new SendAddEditPage(Options));
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.App.Models
|
namespace Bit.App.Models
|
||||||
{
|
{
|
||||||
@ -23,6 +24,7 @@ namespace Bit.App.Models
|
|||||||
public Tuple<SendType, string, byte[], string> CreateSend { get; set; }
|
public Tuple<SendType, string, byte[], string> CreateSend { get; set; }
|
||||||
public bool CopyInsteadOfShareAfterSaving { get; set; }
|
public bool CopyInsteadOfShareAfterSaving { get; set; }
|
||||||
public bool HideAccountSwitcher { get; set; }
|
public bool HideAccountSwitcher { get; set; }
|
||||||
|
public OtpData? OtpData { get; set; }
|
||||||
|
|
||||||
public void SetAllFrom(AppOptions o)
|
public void SetAllFrom(AppOptions o)
|
||||||
{
|
{
|
||||||
@ -48,6 +50,7 @@ namespace Bit.App.Models
|
|||||||
CreateSend = o.CreateSend;
|
CreateSend = o.CreateSend;
|
||||||
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
|
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
|
||||||
HideAccountSwitcher = o.HideAccountSwitcher;
|
HideAccountSwitcher = o.HideAccountSwitcher;
|
||||||
|
OtpData = o.OtpData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,91 +1,29 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.App.Controls;
|
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.CommunityToolkit.ObjectModel;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class AutofillCiphersPageViewModel : BaseViewModel
|
public class AutofillCiphersPageViewModel : CipherSelectionPageViewModel
|
||||||
{
|
{
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private CipherType? _fillType;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
|
||||||
private readonly IAutofillHandler _autofillHandler;
|
|
||||||
private readonly ICipherService _cipherService;
|
|
||||||
private readonly IStateService _stateService;
|
|
||||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
|
||||||
private readonly IMessagingService _messagingService;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
private bool _showNoData;
|
|
||||||
private bool _showList;
|
|
||||||
private string _noDataText;
|
|
||||||
private bool _websiteIconsEnabled;
|
|
||||||
|
|
||||||
public AutofillCiphersPageViewModel()
|
|
||||||
{
|
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
|
||||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
|
||||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
|
||||||
|
|
||||||
GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>();
|
|
||||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
|
||||||
|
|
||||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
|
||||||
{
|
|
||||||
AllowAddAccountRow = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Uri { get; set; }
|
public string Uri { get; set; }
|
||||||
public Command CipherOptionsCommand { get; set; }
|
|
||||||
public bool LoadedOnce { get; set; }
|
|
||||||
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
|
|
||||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
|
||||||
|
|
||||||
public bool ShowNoData
|
public override void Init(AppOptions appOptions)
|
||||||
{
|
|
||||||
get => _showNoData;
|
|
||||||
set => SetProperty(ref _showNoData, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShowList
|
|
||||||
{
|
|
||||||
get => _showList;
|
|
||||||
set => SetProperty(ref _showList, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string NoDataText
|
|
||||||
{
|
|
||||||
get => _noDataText;
|
|
||||||
set => SetProperty(ref _noDataText, value);
|
|
||||||
}
|
|
||||||
public bool WebsiteIconsEnabled
|
|
||||||
{
|
|
||||||
get => _websiteIconsEnabled;
|
|
||||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Init(AppOptions appOptions)
|
|
||||||
{
|
{
|
||||||
Uri = appOptions?.Uri;
|
Uri = appOptions?.Uri;
|
||||||
|
_fillType = appOptions.FillType;
|
||||||
|
|
||||||
string name = null;
|
string name = null;
|
||||||
if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false)
|
if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false)
|
||||||
{
|
{
|
||||||
@ -104,14 +42,11 @@ namespace Bit.App.Pages
|
|||||||
NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--");
|
NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAsync()
|
protected override async Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync()
|
||||||
{
|
{
|
||||||
LoadedOnce = true;
|
|
||||||
ShowList = false;
|
|
||||||
ShowNoData = false;
|
|
||||||
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
|
||||||
var groupedItems = new List<GroupingsPageListGroup>();
|
var groupedItems = new List<GroupingsPageListGroup>();
|
||||||
var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null);
|
var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null);
|
||||||
|
|
||||||
var matching = ciphers.Item1?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
var matching = ciphers.Item1?.Select(c => new GroupingsPageListItem { Cipher = c }).ToList();
|
||||||
var hasMatching = matching?.Any() ?? false;
|
var hasMatching = matching?.Any() ?? false;
|
||||||
if (matching?.Any() ?? false)
|
if (matching?.Any() ?? false)
|
||||||
@ -119,6 +54,7 @@ namespace Bit.App.Pages
|
|||||||
groupedItems.Add(
|
groupedItems.Add(
|
||||||
new GroupingsPageListGroup(matching, AppResources.MatchingItems, matching.Count, false, true));
|
new GroupingsPageListGroup(matching, AppResources.MatchingItems, matching.Count, false, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
var fuzzy = ciphers.Item2?.Select(c =>
|
var fuzzy = ciphers.Item2?.Select(c =>
|
||||||
new GroupingsPageListItem { Cipher = c, FuzzyAutofill = true }).ToList();
|
new GroupingsPageListItem { Cipher = c, FuzzyAutofill = true }).ToList();
|
||||||
if (fuzzy?.Any() ?? false)
|
if (fuzzy?.Any() ?? false)
|
||||||
@ -128,123 +64,88 @@ namespace Bit.App.Pages
|
|||||||
!hasMatching));
|
!hasMatching));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: refactor this
|
return groupedItems;
|
||||||
if (Device.RuntimePlatform == Device.Android
|
|
||||||
||
|
|
||||||
GroupedItems.Any())
|
|
||||||
{
|
|
||||||
var items = new List<IGroupingsPageListItem>();
|
|
||||||
foreach (var itemGroup in groupedItems)
|
|
||||||
{
|
|
||||||
items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount));
|
|
||||||
items.AddRange(itemGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupedItems.ReplaceRange(items);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list
|
|
||||||
var first = true;
|
|
||||||
var items = new List<IGroupingsPageListItem>();
|
|
||||||
foreach (var itemGroup in groupedItems)
|
|
||||||
{
|
|
||||||
if (!first)
|
|
||||||
{
|
|
||||||
items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
items.AddRange(itemGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupedItems.Any())
|
|
||||||
{
|
|
||||||
GroupedItems.ReplaceRange(new List<IGroupingsPageListItem> { new GroupingsPageHeaderListItem(groupedItems[0].Name, groupedItems[0].ItemCount) });
|
|
||||||
GroupedItems.AddRange(items);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GroupedItems.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ShowList = groupedItems.Any();
|
|
||||||
ShowNoData = !ShowList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SelectCipherAsync(CipherView cipher, bool fuzzy)
|
protected override async Task SelectCipherAsync(IGroupingsPageListItem item)
|
||||||
{
|
{
|
||||||
if (cipher == null)
|
if (!(item is GroupingsPageListItem listItem) || listItem.Cipher is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cipher = listItem.Cipher;
|
||||||
|
|
||||||
if (_deviceActionService.SystemMajorVersion() < 21)
|
if (_deviceActionService.SystemMajorVersion() < 21)
|
||||||
{
|
{
|
||||||
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
|
||||||
{
|
{
|
||||||
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
|
return;
|
||||||
|
}
|
||||||
|
var autofillResponse = AppResources.Yes;
|
||||||
|
if (listItem.FuzzyAutofill)
|
||||||
|
{
|
||||||
|
var options = new List<string> { AppResources.Yes };
|
||||||
|
if (cipher.Type == CipherType.Login &&
|
||||||
|
Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
|
||||||
{
|
{
|
||||||
return;
|
options.Add(AppResources.YesAndSave);
|
||||||
}
|
}
|
||||||
var autofillResponse = AppResources.Yes;
|
autofillResponse = await _deviceActionService.DisplayAlertAsync(null,
|
||||||
if (fuzzy)
|
string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, Name), AppResources.No,
|
||||||
|
options.ToArray());
|
||||||
|
}
|
||||||
|
if (autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login)
|
||||||
|
{
|
||||||
|
var uris = cipher.Login?.Uris?.ToList();
|
||||||
|
if (uris == null)
|
||||||
{
|
{
|
||||||
var options = new List<string> { AppResources.Yes };
|
uris = new List<LoginUriView>();
|
||||||
if (cipher.Type == CipherType.Login &&
|
|
||||||
Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
|
|
||||||
{
|
|
||||||
options.Add(AppResources.YesAndSave);
|
|
||||||
}
|
|
||||||
autofillResponse = await _deviceActionService.DisplayAlertAsync(null,
|
|
||||||
string.Format(AppResources.BitwardenAutofillServiceMatchConfirm, Name), AppResources.No,
|
|
||||||
options.ToArray());
|
|
||||||
}
|
}
|
||||||
if (autofillResponse == AppResources.YesAndSave && cipher.Type == CipherType.Login)
|
uris.Add(new LoginUriView
|
||||||
{
|
{
|
||||||
var uris = cipher.Login?.Uris?.ToList();
|
Uri = Uri,
|
||||||
if (uris == null)
|
Match = null
|
||||||
|
});
|
||||||
|
cipher.Login.Uris = uris;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||||
|
await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher));
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
}
|
||||||
|
catch (ApiException e)
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
if (e?.Error != null)
|
||||||
{
|
{
|
||||||
uris = new List<LoginUriView>();
|
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||||
}
|
AppResources.AnErrorHasOccurred);
|
||||||
uris.Add(new LoginUriView
|
|
||||||
{
|
|
||||||
Uri = Uri,
|
|
||||||
Match = null
|
|
||||||
});
|
|
||||||
cipher.Login.Uris = uris;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
|
||||||
await _cipherService.SaveWithServerAsync(await _cipherService.EncryptAsync(cipher));
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
}
|
|
||||||
catch (ApiException e)
|
|
||||||
{
|
|
||||||
await _deviceActionService.HideLoadingAsync();
|
|
||||||
if (e?.Error != null)
|
|
||||||
{
|
|
||||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
|
||||||
AppResources.AnErrorHasOccurred);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
|
}
|
||||||
{
|
if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
|
||||||
_autofillHandler.Autofill(cipher);
|
{
|
||||||
}
|
_autofillHandler.Autofill(cipher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void CipherOptionsAsync(CipherView cipher)
|
protected override async Task AddCipherAsync()
|
||||||
{
|
{
|
||||||
if ((Page as BaseContentPage).DoOnce())
|
if (_fillType.HasValue && _fillType != CipherType.Login)
|
||||||
{
|
{
|
||||||
await AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
var pageForOther = new CipherAddEditPage(type: _fillType, fromAutofill: true);
|
||||||
|
await Page.Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: Uri, name: Name,
|
||||||
|
fromAutofill: true);
|
||||||
|
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Lists.ItemViewModels.CustomFields;
|
using Bit.App.Lists.ItemViewModels.CustomFields;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
@ -30,6 +31,7 @@ namespace Bit.App.Pages
|
|||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly IAutofillHandler _autofillHandler;
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
private readonly IWatchDeviceService _watchDeviceService;
|
private readonly IWatchDeviceService _watchDeviceService;
|
||||||
|
private readonly IAccountsManager _accountsManager;
|
||||||
|
|
||||||
private bool _showNotesSeparator;
|
private bool _showNotesSeparator;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
@ -44,6 +46,8 @@ namespace Bit.App.Pages
|
|||||||
private bool _hasCollections;
|
private bool _hasCollections;
|
||||||
private string _previousCipherId;
|
private string _previousCipherId;
|
||||||
private List<Core.Models.View.CollectionView> _writeableCollections;
|
private List<Core.Models.View.CollectionView> _writeableCollections;
|
||||||
|
private bool _fromOtp;
|
||||||
|
|
||||||
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
||||||
{
|
{
|
||||||
nameof(IsLogin),
|
nameof(IsLogin),
|
||||||
@ -82,6 +86,8 @@ namespace Bit.App.Pages
|
|||||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||||
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
||||||
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||||
|
|
||||||
|
|
||||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
@ -302,6 +308,7 @@ namespace Bit.App.Pages
|
|||||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||||
public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
|
public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
|
||||||
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
|
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
|
||||||
|
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
|
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
|
||||||
@ -309,6 +316,8 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public async Task<bool> LoadAsync(AppOptions appOptions = null)
|
public async Task<bool> LoadAsync(AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
|
_fromOtp = appOptions?.OtpData != null;
|
||||||
|
|
||||||
var myEmail = await _stateService.GetEmailAsync();
|
var myEmail = await _stateService.GetEmailAsync();
|
||||||
OwnershipOptions.Add(new KeyValuePair<string, string>(myEmail, null));
|
OwnershipOptions.Add(new KeyValuePair<string, string>(myEmail, null));
|
||||||
var orgs = await _organizationService.GetAllAsync();
|
var orgs = await _organizationService.GetAllAsync();
|
||||||
@ -358,6 +367,10 @@ namespace Bit.App.Pages
|
|||||||
Cipher.OrganizationId = OrganizationId;
|
Cipher.OrganizationId = OrganizationId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (appOptions?.OtpData != null && Cipher.Type == CipherType.Login)
|
||||||
|
{
|
||||||
|
Cipher.Login.Totp = appOptions.OtpData.Value.Uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -380,6 +393,7 @@ namespace Bit.App.Pages
|
|||||||
Cipher.Type = appOptions.SaveType.GetValueOrDefault(Cipher.Type);
|
Cipher.Type = appOptions.SaveType.GetValueOrDefault(Cipher.Type);
|
||||||
Cipher.Login.Username = appOptions.SaveUsername;
|
Cipher.Login.Username = appOptions.SaveUsername;
|
||||||
Cipher.Login.Password = appOptions.SavePassword;
|
Cipher.Login.Password = appOptions.SavePassword;
|
||||||
|
Cipher.Login.Totp = appOptions.OtpData?.Uri;
|
||||||
Cipher.Card.Code = appOptions.SaveCardCode;
|
Cipher.Card.Code = appOptions.SaveCardCode;
|
||||||
if (int.TryParse(appOptions.SaveCardExpMonth, out int month) && month <= 12 && month >= 1)
|
if (int.TryParse(appOptions.SaveCardExpMonth, out int month) && month <= 12 && month >= 1)
|
||||||
{
|
{
|
||||||
@ -424,6 +438,11 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
|
Fields.ResetWithRange(Cipher.Fields?.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, true, Cipher, null, null, FieldOptionsCommand)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appOptions?.OtpData != null)
|
||||||
|
{
|
||||||
|
_platformUtilsService.ShowToast(null, AppResources.AuthenticatorKey, AppResources.AuthenticatorKeyAdded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (EditMode && _previousCipherId != CipherId)
|
if (EditMode && _previousCipherId != CipherId)
|
||||||
@ -517,6 +536,10 @@ namespace Bit.App.Pages
|
|||||||
// Close and go back to app
|
// Close and go back to app
|
||||||
_autofillHandler.CloseAutofill();
|
_autofillHandler.CloseAutofill();
|
||||||
}
|
}
|
||||||
|
else if (_fromOtp)
|
||||||
|
{
|
||||||
|
await _accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (CloneMode)
|
if (CloneMode)
|
||||||
|
@ -1,30 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<pages:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Bit.App.Pages.AutofillCiphersPage"
|
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
|
||||||
|
x:Class="Bit.App.Pages.CipherSelectionPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
xmlns:effects="clr-namespace:Bit.App.Effects;assembly=BitwardenApp"
|
xmlns:effects="clr-namespace:Bit.App.Effects;assembly=BitwardenApp"
|
||||||
x:DataType="pages:AutofillCiphersPageViewModel"
|
x:DataType="pages:CipherSelectionPageViewModel"
|
||||||
Title="{Binding PageTitle}"
|
Title="{Binding PageTitle}"
|
||||||
x:Name="_page">
|
x:Name="_page">
|
||||||
|
|
||||||
<ContentPage.BindingContext>
|
|
||||||
<pages:AutofillCiphersPageViewModel />
|
|
||||||
</ContentPage.BindingContext>
|
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<controls:ExtendedToolbarItem
|
<controls:ExtendedToolbarItem
|
||||||
x:Name="_accountAvatar"
|
x:Name="_accountAvatar"
|
||||||
IconImageSource="{Binding AvatarImageSource}"
|
IconImageSource="{Binding AvatarImageSource}"
|
||||||
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
Command="{Binding Source={x:Reference _accountListOverlay}, Path=ToggleVisibililtyCommand}"
|
||||||
Order="Primary"
|
Order="Primary"
|
||||||
Priority="-1"
|
Priority="-2"
|
||||||
UseOriginalImage="True"
|
UseOriginalImage="True"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Account}" />
|
AutomationProperties.Name="{u:I18n Account}" />
|
||||||
<ToolbarItem Icon="search.png" Clicked="Search_Clicked"
|
<ToolbarItem IconImageSource="search.png" Clicked="Search_Clicked"
|
||||||
AutomationProperties.IsInAccessibleTree="True"
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
AutomationProperties.Name="{u:I18n Search}" />
|
AutomationProperties.Name="{u:I18n Search}" />
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
@ -32,6 +28,21 @@
|
|||||||
<ContentPage.Resources>
|
<ContentPage.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||||
|
<controls:SelectionChangedEventArgsConverter x:Key="SelectionChangedEventArgsConverter" />
|
||||||
|
|
||||||
|
<ToolbarItem
|
||||||
|
x:Name="_closeItem"
|
||||||
|
x:Key="_closeItem"
|
||||||
|
Text="{u:I18n Close}"
|
||||||
|
Clicked="CloseItem_Clicked"
|
||||||
|
Order="Primary"
|
||||||
|
Priority="-1" />
|
||||||
|
<ToolbarItem x:Name="_addItem" x:Key="addItem"
|
||||||
|
IconImageSource="plus.png"
|
||||||
|
Command="{Binding AddCipherCommand}"
|
||||||
|
Order="Primary"
|
||||||
|
AutomationProperties.IsInAccessibleTree="True"
|
||||||
|
AutomationProperties.Name="{u:I18n AddItem}" />
|
||||||
|
|
||||||
<DataTemplate x:Key="cipherTemplate"
|
<DataTemplate x:Key="cipherTemplate"
|
||||||
x:DataType="pages:GroupingsPageListItem">
|
x:DataType="pages:GroupingsPageListItem">
|
||||||
@ -70,23 +81,44 @@
|
|||||||
Padding="20, 0"
|
Padding="20, 0"
|
||||||
Spacing="20"
|
Spacing="20"
|
||||||
IsVisible="{Binding ShowNoData}">
|
IsVisible="{Binding ShowNoData}">
|
||||||
|
<Image
|
||||||
|
Source="empty_items_state" />
|
||||||
<Label
|
<Label
|
||||||
Text="{Binding NoDataText}"
|
Text="{Binding NoDataText}"
|
||||||
HorizontalTextAlignment="Center"></Label>
|
HorizontalTextAlignment="Center"></Label>
|
||||||
<Button
|
<Button
|
||||||
Text="{u:I18n AddAnItem}"
|
Text="{u:I18n AddAnItem}"
|
||||||
Clicked="AddButton_Clicked"></Button>
|
Command="{Binding AddCipherCommand}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|
||||||
|
<Frame
|
||||||
|
IsVisible="{Binding ShowCallout}"
|
||||||
|
Padding="10"
|
||||||
|
Margin="20, 10"
|
||||||
|
HasShadow="False"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
BorderColor="{DynamicResource PrimaryColor}">
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n AddTheKeyToAnExistingOrNewItem}"
|
||||||
|
StyleClass="text-muted, text-sm, text-bold"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
</Frame>
|
||||||
|
|
||||||
<controls:ExtendedCollectionView
|
<controls:ExtendedCollectionView
|
||||||
IsVisible="{Binding ShowList}"
|
IsVisible="{Binding ShowList}"
|
||||||
ItemsSource="{Binding GroupedItems}"
|
ItemsSource="{Binding GroupedItems}"
|
||||||
VerticalOptions="FillAndExpand"
|
VerticalOptions="FillAndExpand"
|
||||||
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
ItemTemplate="{StaticResource listItemDataTemplateSelector}"
|
||||||
SelectionMode="Single"
|
SelectionMode="Single"
|
||||||
SelectionChanged="RowSelected"
|
|
||||||
StyleClass="list, list-platform"
|
StyleClass="list, list-platform"
|
||||||
ExtraDataForLogging="Autofill Ciphers Page" />
|
ExtraDataForLogging="Autofill Ciphers Page">
|
||||||
|
<controls:ExtendedCollectionView.Behaviors>
|
||||||
|
<xct:EventToCommandBehavior
|
||||||
|
EventName="SelectionChanged"
|
||||||
|
Command="{Binding SelectCipherCommand}"
|
||||||
|
EventArgsConverter="{StaticResource SelectionChangedEventArgsConverter}" />
|
||||||
|
</controls:ExtendedCollectionView.Behaviors>
|
||||||
|
</controls:ExtendedCollectionView>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</ContentPage.Resources>
|
</ContentPage.Resources>
|
||||||
@ -104,9 +136,10 @@
|
|||||||
<!-- Android FAB -->
|
<!-- Android FAB -->
|
||||||
<Button
|
<Button
|
||||||
x:Name="_fab"
|
x:Name="_fab"
|
||||||
Image="plus.png"
|
ImageSource="plus.png"
|
||||||
Clicked="AddButton_Clicked"
|
Command="{Binding AddCipherCommand}"
|
||||||
Style="{StaticResource btn-fab}"
|
Style="{StaticResource btn-fab}"
|
||||||
|
IsVisible="{OnPlatform iOS=false, Android=true}"
|
||||||
AbsoluteLayout.LayoutFlags="PositionProportional"
|
AbsoluteLayout.LayoutFlags="PositionProportional"
|
||||||
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize">
|
AbsoluteLayout.LayoutBounds="1, 1, AutoSize, AutoSize">
|
||||||
<Button.Effects>
|
<Button.Effects>
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
@ -12,27 +11,46 @@ using Xamarin.Forms;
|
|||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public partial class AutofillCiphersPage : BaseContentPage
|
public partial class CipherSelectionPage : BaseContentPage
|
||||||
{
|
{
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private readonly IBroadcasterService _broadcasterService;
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
private readonly IAccountsManager _accountsManager;
|
||||||
|
|
||||||
private AutofillCiphersPageViewModel _vm;
|
private readonly CipherSelectionPageViewModel _vm;
|
||||||
|
|
||||||
public AutofillCiphersPage(AppOptions appOptions)
|
public CipherSelectionPage(AppOptions appOptions)
|
||||||
{
|
{
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
|
|
||||||
|
if (appOptions?.OtpData is null)
|
||||||
|
{
|
||||||
|
BindingContext = new AutofillCiphersPageViewModel();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BindingContext = new OTPCipherSelectionPageViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_closeItem);
|
||||||
|
ToolbarItems.Add(_addItem);
|
||||||
|
}
|
||||||
|
|
||||||
SetActivityIndicator(_mainContent);
|
SetActivityIndicator(_mainContent);
|
||||||
_vm = BindingContext as AutofillCiphersPageViewModel;
|
_vm = BindingContext as CipherSelectionPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.Init(appOptions);
|
_vm.Init(appOptions);
|
||||||
|
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async override void OnAppearing()
|
protected async override void OnAppearing()
|
||||||
@ -51,10 +69,16 @@ namespace Bit.App.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_accountAvatar?.OnAppearing();
|
// TODO: There's currently an issue on iOS where the toolbar item is not getting updated
|
||||||
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
// as the others somehow. Removing this so at least we get the circle with ".." instead
|
||||||
|
// of a white circle
|
||||||
|
if (Device.RuntimePlatform != Device.iOS)
|
||||||
|
{
|
||||||
|
_accountAvatar?.OnAppearing();
|
||||||
|
_vm.AvatarImageSource = await GetAvatarImageSourceAsync();
|
||||||
|
}
|
||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(AutofillCiphersPage), async (message) =>
|
_broadcasterService.Subscribe(nameof(CipherSelectionPage), async (message) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -116,40 +140,44 @@ namespace Bit.App.Pages
|
|||||||
_accountAvatar?.OnDisappearing();
|
_accountAvatar?.OnDisappearing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
private void AddButton_Clicked(object sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
|
||||||
if (!DoOnce())
|
if (!DoOnce())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item && item.Cipher != null)
|
|
||||||
|
if (_vm is AutofillCiphersPageViewModel autofillVM)
|
||||||
{
|
{
|
||||||
await _vm.SelectCipherAsync(item.Cipher, item.FuzzyAutofill);
|
AddFromAutofill(autofillVM).FireAndForget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void AddButton_Clicked(object sender, System.EventArgs e)
|
private async Task AddFromAutofill(AutofillCiphersPageViewModel autofillVM)
|
||||||
{
|
{
|
||||||
if (!DoOnce())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
|
if (_appOptions.FillType.HasValue && _appOptions.FillType != CipherType.Login)
|
||||||
{
|
{
|
||||||
var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
var pageForOther = new CipherAddEditPage(type: _appOptions.FillType, fromAutofill: true);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
await Navigation.PushModalAsync(new NavigationPage(pageForOther));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: _vm.Uri, name: _vm.Name,
|
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, uri: autofillVM.Uri, name: _vm.Name,
|
||||||
fromAutofill: true);
|
fromAutofill: true);
|
||||||
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
await Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Search_Clicked(object sender, System.EventArgs e)
|
private void Search_Clicked(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var page = new CiphersPage(null, autofillUrl: _vm.Uri);
|
var page = new CiphersPage(null, appOptions: _appOptions);
|
||||||
Application.Current.MainPage = new NavigationPage(page);
|
Navigation.PushModalAsync(new NavigationPage(page)).FireAndForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseItem_Clicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
_accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null).FireAndForget();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
167
src/App/Pages/Vault/CipherSelectionPageViewModel.cs
Normal file
167
src/App/Pages/Vault/CipherSelectionPageViewModel.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public abstract class CipherSelectionPageViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
protected readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
protected readonly IDeviceActionService _deviceActionService;
|
||||||
|
protected readonly IAutofillHandler _autofillHandler;
|
||||||
|
protected readonly ICipherService _cipherService;
|
||||||
|
protected readonly IStateService _stateService;
|
||||||
|
protected readonly IPasswordRepromptService _passwordRepromptService;
|
||||||
|
protected readonly IMessagingService _messagingService;
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
|
protected bool _showNoData;
|
||||||
|
protected bool _showList;
|
||||||
|
protected string _noDataText;
|
||||||
|
protected bool _websiteIconsEnabled;
|
||||||
|
|
||||||
|
public CipherSelectionPageViewModel()
|
||||||
|
{
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
|
||||||
|
_cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||||
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
|
||||||
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
|
_stateService = ServiceContainer.Resolve<IStateService>();
|
||||||
|
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>();
|
||||||
|
_messagingService = ServiceContainer.Resolve<IMessagingService>();
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>();
|
||||||
|
|
||||||
|
GroupedItems = new ObservableRangeCollection<IGroupingsPageListItem>();
|
||||||
|
CipherOptionsCommand = new AsyncCommand<CipherView>(cipher => AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService),
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
SelectCipherCommand = new AsyncCommand<IGroupingsPageListItem>(SelectCipherAsync,
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
AddCipherCommand = new AsyncCommand(AddCipherAsync,
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
|
||||||
|
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||||
|
{
|
||||||
|
AllowAddAccountRow = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool LoadedOnce { get; set; }
|
||||||
|
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
|
||||||
|
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||||
|
|
||||||
|
public ICommand CipherOptionsCommand { get; set; }
|
||||||
|
public ICommand SelectCipherCommand { get; set; }
|
||||||
|
public ICommand AddCipherCommand { get; set; }
|
||||||
|
|
||||||
|
public bool ShowNoData
|
||||||
|
{
|
||||||
|
get => _showNoData;
|
||||||
|
set => SetProperty(ref _showNoData, value, additionalPropertyNames: new string[] { nameof(ShowCallout) });
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowList
|
||||||
|
{
|
||||||
|
get => _showList;
|
||||||
|
set => SetProperty(ref _showList, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NoDataText
|
||||||
|
{
|
||||||
|
get => _noDataText;
|
||||||
|
set => SetProperty(ref _noDataText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WebsiteIconsEnabled
|
||||||
|
{
|
||||||
|
get => _websiteIconsEnabled;
|
||||||
|
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool ShowCallout => false;
|
||||||
|
|
||||||
|
public abstract void Init(Models.AppOptions options);
|
||||||
|
|
||||||
|
public async Task LoadAsync()
|
||||||
|
{
|
||||||
|
LoadedOnce = true;
|
||||||
|
ShowList = false;
|
||||||
|
ShowNoData = false;
|
||||||
|
WebsiteIconsEnabled = !(await _stateService.GetDisableFaviconAsync()).GetValueOrDefault();
|
||||||
|
|
||||||
|
var groupedItems = await LoadGroupedItemsAsync();
|
||||||
|
|
||||||
|
// TODO: refactor this
|
||||||
|
if (Device.RuntimePlatform == Device.Android
|
||||||
|
||
|
||||||
|
GroupedItems.Any())
|
||||||
|
{
|
||||||
|
var items = new List<IGroupingsPageListItem>();
|
||||||
|
foreach (var itemGroup in groupedItems)
|
||||||
|
{
|
||||||
|
items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount));
|
||||||
|
items.AddRange(itemGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupedItems.ReplaceRange(items);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// HACK: we need this on iOS, so that it doesn't crash when adding coming from an empty list
|
||||||
|
var first = true;
|
||||||
|
var items = new List<IGroupingsPageListItem>();
|
||||||
|
foreach (var itemGroup in groupedItems)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
items.Add(new GroupingsPageHeaderListItem(itemGroup.Name, itemGroup.ItemCount));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
items.AddRange(itemGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Device.InvokeOnMainThreadAsync(() =>
|
||||||
|
{
|
||||||
|
if (groupedItems.Any())
|
||||||
|
{
|
||||||
|
GroupedItems.ReplaceRange(new List<IGroupingsPageListItem> { new GroupingsPageHeaderListItem(groupedItems[0].Name, groupedItems[0].ItemCount) });
|
||||||
|
GroupedItems.AddRange(items);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GroupedItems.Clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await Device.InvokeOnMainThreadAsync(() =>
|
||||||
|
{
|
||||||
|
ShowList = groupedItems.Any();
|
||||||
|
ShowNoData = !ShowList;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync();
|
||||||
|
|
||||||
|
protected abstract Task SelectCipherAsync(IGroupingsPageListItem item);
|
||||||
|
|
||||||
|
protected abstract Task AddCipherAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -81,12 +81,22 @@
|
|||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
HorizontalOptions="CenterAndExpand"
|
HorizontalOptions="CenterAndExpand"
|
||||||
HorizontalTextAlignment="Center" />
|
HorizontalTextAlignment="Center" />
|
||||||
<Label IsVisible="{Binding ShowNoData}"
|
<StackLayout
|
||||||
Text="{u:I18n NoItemsToList}"
|
HorizontalOptions="Center"
|
||||||
Margin="20, 0"
|
VerticalOptions="StartAndExpand"
|
||||||
VerticalOptions="CenterAndExpand"
|
Margin="20, 80, 20, 0"
|
||||||
HorizontalOptions="CenterAndExpand"
|
Spacing="20"
|
||||||
HorizontalTextAlignment="Center" />
|
IsVisible="{Binding ShowNoData}">
|
||||||
|
<Image
|
||||||
|
Source="empty_items_state" />
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n ThereAreNoItemsThatMatchTheSearch}"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n AddAnItem}"
|
||||||
|
Command="{Binding AddCipherCommand}"
|
||||||
|
IsVisible="{Binding ShowAddCipher}"/>
|
||||||
|
</StackLayout>
|
||||||
<controls:ExtendedCollectionView
|
<controls:ExtendedCollectionView
|
||||||
IsVisible="{Binding ShowList}"
|
IsVisible="{Binding ShowList}"
|
||||||
ItemsSource="{Binding Ciphers}"
|
ItemsSource="{Binding Ciphers}"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
@ -17,15 +18,18 @@ namespace Bit.App.Pages
|
|||||||
private CiphersPageViewModel _vm;
|
private CiphersPageViewModel _vm;
|
||||||
private bool _hasFocused;
|
private bool _hasFocused;
|
||||||
|
|
||||||
public CiphersPage(Func<CipherView, bool> filter, string pageTitle = null, string vaultFilterSelection = null,
|
public CiphersPage(Func<CipherView, bool> filter,
|
||||||
string autofillUrl = null, bool deleted = false)
|
string pageTitle = null,
|
||||||
|
string vaultFilterSelection = null,
|
||||||
|
bool deleted = false,
|
||||||
|
AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as CiphersPageViewModel;
|
_vm = BindingContext as CiphersPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
_vm.Filter = filter;
|
_autofillUrl = appOptions?.Uri;
|
||||||
_vm.AutofillUrl = _autofillUrl = autofillUrl;
|
_vm.Prepare(filter, deleted, appOptions);
|
||||||
_vm.Deleted = deleted;
|
|
||||||
if (pageTitle != null)
|
if (pageTitle != null)
|
||||||
{
|
{
|
||||||
_vm.PageTitle = string.Format(AppResources.SearchGroup, pageTitle);
|
_vm.PageTitle = string.Format(AppResources.SearchGroup, pageTitle);
|
||||||
|
@ -3,13 +3,16 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Models;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
@ -31,6 +34,7 @@ namespace Bit.App.Pages
|
|||||||
private bool _showNoData;
|
private bool _showNoData;
|
||||||
private bool _showList;
|
private bool _showList;
|
||||||
private bool _websiteIconsEnabled;
|
private bool _websiteIconsEnabled;
|
||||||
|
private AppOptions _appOptions;
|
||||||
|
|
||||||
public CiphersPageViewModel()
|
public CiphersPageViewModel()
|
||||||
{
|
{
|
||||||
@ -46,14 +50,21 @@ namespace Bit.App.Pages
|
|||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
|
||||||
Ciphers = new ExtendedObservableCollection<CipherView>();
|
Ciphers = new ExtendedObservableCollection<CipherView>();
|
||||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
CipherOptionsCommand = new AsyncCommand<CipherView>(cipher => Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService),
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
AddCipherCommand = new AsyncCommand(AddCipherAsync,
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Command CipherOptionsCommand { get; set; }
|
public ICommand CipherOptionsCommand { get; }
|
||||||
|
public ICommand AddCipherCommand { get; }
|
||||||
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
|
public ExtendedObservableCollection<CipherView> Ciphers { get; set; }
|
||||||
public Func<CipherView, bool> Filter { get; set; }
|
public Func<CipherView, bool> Filter { get; set; }
|
||||||
public string AutofillUrl { get; set; }
|
public string AutofillUrl { get; set; }
|
||||||
public bool Deleted { get; set; }
|
public bool Deleted { get; set; }
|
||||||
|
public bool ShowAllIfSearchTextEmpty { get; set; }
|
||||||
|
|
||||||
protected override ICipherService cipherService => _cipherService;
|
protected override ICipherService cipherService => _cipherService;
|
||||||
protected override IPolicyService policyService => _policyService;
|
protected override IPolicyService policyService => _policyService;
|
||||||
@ -65,7 +76,8 @@ namespace Bit.App.Pages
|
|||||||
get => _showNoData;
|
get => _showNoData;
|
||||||
set => SetProperty(ref _showNoData, value, additionalPropertyNames: new string[]
|
set => SetProperty(ref _showNoData, value, additionalPropertyNames: new string[]
|
||||||
{
|
{
|
||||||
nameof(ShowSearchDirection)
|
nameof(ShowSearchDirection),
|
||||||
|
nameof(ShowAddCipher)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,12 +92,23 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
public bool ShowSearchDirection => !ShowList && !ShowNoData;
|
public bool ShowSearchDirection => !ShowList && !ShowNoData;
|
||||||
|
|
||||||
|
public bool ShowAddCipher => ShowNoData && _appOptions?.OtpData != null;
|
||||||
|
|
||||||
public bool WebsiteIconsEnabled
|
public bool WebsiteIconsEnabled
|
||||||
{
|
{
|
||||||
get => _websiteIconsEnabled;
|
get => _websiteIconsEnabled;
|
||||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void Prepare(Func<CipherView, bool> filter, bool deleted, AppOptions appOptions)
|
||||||
|
{
|
||||||
|
Filter = filter;
|
||||||
|
AutofillUrl = appOptions?.Uri;
|
||||||
|
Deleted = deleted;
|
||||||
|
ShowAllIfSearchTextEmpty = appOptions?.OtpData != null;
|
||||||
|
_appOptions = appOptions;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task InitAsync()
|
public async Task InitAsync()
|
||||||
{
|
{
|
||||||
await InitVaultFilterAsync(true);
|
await InitVaultFilterAsync(true);
|
||||||
@ -101,25 +124,33 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
List<CipherView> ciphers = null;
|
List<CipherView> ciphers = null;
|
||||||
var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1;
|
var searchable = !string.IsNullOrWhiteSpace(searchText) && searchText.Length > 1;
|
||||||
if (searchable)
|
var shouldShowAllWhenEmpty = ShowAllIfSearchTextEmpty && string.IsNullOrEmpty(searchText);
|
||||||
|
if (searchable || shouldShowAllWhenEmpty)
|
||||||
{
|
{
|
||||||
if (timeout != null)
|
if (timeout != null)
|
||||||
{
|
{
|
||||||
await Task.Delay(timeout.Value);
|
await Task.Delay(timeout.Value);
|
||||||
}
|
}
|
||||||
if (searchText != (Page as CiphersPage).SearchBar.Text)
|
if (searchText != (Page as CiphersPage).SearchBar.Text
|
||||||
|
&&
|
||||||
|
!shouldShowAllWhenEmpty)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
previousCts?.Cancel();
|
||||||
previousCts?.Cancel();
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var vaultFilteredCiphers = await GetAllCiphersAsync();
|
var vaultFilteredCiphers = await GetAllCiphersAsync();
|
||||||
ciphers = await _searchService.SearchCiphersAsync(searchText,
|
if (!shouldShowAllWhenEmpty)
|
||||||
Filter ?? (c => c.IsDeleted == Deleted), vaultFilteredCiphers, cts.Token);
|
{
|
||||||
|
ciphers = await _searchService.SearchCiphersAsync(searchText,
|
||||||
|
Filter ?? (c => c.IsDeleted == Deleted), vaultFilteredCiphers, cts.Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ciphers = vaultFilteredCiphers;
|
||||||
|
}
|
||||||
cts.Token.ThrowIfCancellationRequested();
|
cts.Token.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@ -134,8 +165,8 @@ namespace Bit.App.Pages
|
|||||||
Device.BeginInvokeOnMainThread(() =>
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
{
|
{
|
||||||
Ciphers.ResetWithRange(ciphers);
|
Ciphers.ResetWithRange(ciphers);
|
||||||
ShowNoData = searchable && Ciphers.Count == 0;
|
ShowNoData = !shouldShowAllWhenEmpty && searchable && Ciphers.Count == 0;
|
||||||
ShowList = searchable && !ShowNoData;
|
ShowList = (searchable || shouldShowAllWhenEmpty) && !ShowNoData;
|
||||||
});
|
});
|
||||||
}, cts.Token);
|
}, cts.Token);
|
||||||
_searchCancellationTokenSource = cts;
|
_searchCancellationTokenSource = cts;
|
||||||
@ -144,6 +175,7 @@ namespace Bit.App.Pages
|
|||||||
public async Task SelectCipherAsync(CipherView cipher)
|
public async Task SelectCipherAsync(CipherView cipher)
|
||||||
{
|
{
|
||||||
string selection = null;
|
string selection = null;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(AutofillUrl))
|
if (!string.IsNullOrWhiteSpace(AutofillUrl))
|
||||||
{
|
{
|
||||||
var options = new List<string> { AppResources.Autofill };
|
var options = new List<string> { AppResources.Autofill };
|
||||||
@ -156,6 +188,19 @@ namespace Bit.App.Pages
|
|||||||
selection = await Page.DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
|
selection = await Page.DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
|
||||||
options.ToArray());
|
options.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_appOptions?.OtpData != null)
|
||||||
|
{
|
||||||
|
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var editCipherPage = new CipherAddEditPage(cipher.Id, appOptions: _appOptions);
|
||||||
|
await Page.Navigation.PushModalAsync(new NavigationPage(editCipherPage));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
|
if (selection == AppResources.View || string.IsNullOrWhiteSpace(AutofillUrl))
|
||||||
{
|
{
|
||||||
var page = new CipherDetailsPage(cipher.Id);
|
var page = new CipherDetailsPage(cipher.Id);
|
||||||
@ -205,7 +250,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private void PerformSearchIfPopulated()
|
private void PerformSearchIfPopulated()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text))
|
if (!string.IsNullOrWhiteSpace((Page as CiphersPage).SearchBar.Text) || ShowAllIfSearchTextEmpty)
|
||||||
{
|
{
|
||||||
Search((Page as CiphersPage).SearchBar.Text, 200);
|
Search((Page as CiphersPage).SearchBar.Text, 200);
|
||||||
}
|
}
|
||||||
@ -216,12 +261,10 @@ namespace Bit.App.Pages
|
|||||||
PerformSearchIfPopulated();
|
PerformSearchIfPopulated();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void CipherOptionsAsync(CipherView cipher)
|
private async Task AddCipherAsync()
|
||||||
{
|
{
|
||||||
if ((Page as BaseContentPage).DoOnce())
|
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, name: _appOptions?.OtpData?.Issuer ?? _appOptions?.OtpData?.AccountName, appOptions: _appOptions);
|
||||||
{
|
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||||
await Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
79
src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs
Normal file
79
src/App/Pages/Vault/OTPCipherSelectionPageViewModel.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class OTPCipherSelectionPageViewModel : CipherSelectionPageViewModel
|
||||||
|
{
|
||||||
|
private readonly ISearchService _searchService = ServiceContainer.Resolve<ISearchService>();
|
||||||
|
|
||||||
|
private OtpData _otpData;
|
||||||
|
private Models.AppOptions _appOptions;
|
||||||
|
|
||||||
|
public override bool ShowCallout => !ShowNoData;
|
||||||
|
|
||||||
|
public override void Init(Models.AppOptions options)
|
||||||
|
{
|
||||||
|
_appOptions = options;
|
||||||
|
_otpData = options.OtpData.Value;
|
||||||
|
|
||||||
|
Name = _otpData.Issuer ?? _otpData.AccountName;
|
||||||
|
PageTitle = string.Format(AppResources.ItemsForUri, Name ?? "--");
|
||||||
|
NoDataText = string.Format(AppResources.ThereAreNoItemsInYourVaultThatMatchX, Name ?? "--")
|
||||||
|
+ Environment.NewLine
|
||||||
|
+ AppResources.SearchForAnItemOrAddANewItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync()
|
||||||
|
{
|
||||||
|
var groupedItems = new List<GroupingsPageListGroup>();
|
||||||
|
var allCiphers = await _cipherService.GetAllDecryptedAsync();
|
||||||
|
var ciphers = await _searchService.SearchCiphersAsync(_otpData.Issuer ?? _otpData.AccountName,
|
||||||
|
c => c.Type == CipherType.Login && !c.IsDeleted, allCiphers);
|
||||||
|
|
||||||
|
if (ciphers?.Any() ?? false)
|
||||||
|
{
|
||||||
|
groupedItems.Add(
|
||||||
|
new GroupingsPageListGroup(ciphers.Select(c => new GroupingsPageListItem { Cipher = c }).ToList(),
|
||||||
|
AppResources.MatchingItems,
|
||||||
|
ciphers.Count,
|
||||||
|
false,
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task SelectCipherAsync(IGroupingsPageListItem item)
|
||||||
|
{
|
||||||
|
if (!(item is GroupingsPageListItem listItem) || listItem.Cipher is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cipher = listItem.Cipher;
|
||||||
|
|
||||||
|
if (cipher.Reprompt != CipherRepromptType.None && !await _passwordRepromptService.ShowPasswordPromptAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var editCipherPage = new CipherAddEditPage(cipher.Id, appOptions: _appOptions);
|
||||||
|
await Page.Navigation.PushModalAsync(new NavigationPage(editCipherPage));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task AddCipherAsync()
|
||||||
|
{
|
||||||
|
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, name: Name, appOptions: _appOptions);
|
||||||
|
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
src/App/Resources/AppResources.Designer.cs
generated
47
src/App/Resources/AppResources.Designer.cs
generated
@ -1,7 +1,6 @@
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// This code was generated by a tool.
|
// This code was generated by a tool.
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
//
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
// the code is regenerated.
|
// the code is regenerated.
|
||||||
@ -14,12 +13,10 @@ namespace Bit.App.Resources {
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// This class was generated by MSBuild using the GenerateResource task.
|
||||||
|
/// To add or remove a member, edit your .resx file then rerun MSBuild.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")]
|
||||||
// class via a tool like ResGen or Visual Studio.
|
|
||||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
|
||||||
// with the /str option, or rebuild your VS project.
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
public class AppResources {
|
public class AppResources {
|
||||||
@ -385,6 +382,15 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Add the key to an existing or new item.
|
||||||
|
/// </summary>
|
||||||
|
public static string AddTheKeyToAnExistingOrNewItem {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AddTheKeyToAnExistingOrNewItem", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Add TOTP.
|
/// Looks up a localized string similar to Add TOTP.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -5339,6 +5345,15 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Search for an item or add a new item.
|
||||||
|
/// </summary>
|
||||||
|
public static string SearchForAnItemOrAddANewItem {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SearchForAnItemOrAddANewItem", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Search {0}.
|
/// Looks up a localized string similar to Search {0}.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -6032,6 +6047,24 @@ namespace Bit.App.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to There are no items in your vault that match "{0}".
|
||||||
|
/// </summary>
|
||||||
|
public static string ThereAreNoItemsInYourVaultThatMatchX {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ThereAreNoItemsInYourVaultThatMatchX", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to There are no items that match the search.
|
||||||
|
/// </summary>
|
||||||
|
public static string ThereAreNoItemsThatMatchTheSearch {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ThereAreNoItemsThatMatchTheSearch", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to 30 days.
|
/// Looks up a localized string similar to 30 days.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -2592,4 +2592,16 @@ Do you want to switch to this account?</value>
|
|||||||
<data name="OrganizationSsoIdentifierRequired" xml:space="preserve">
|
<data name="OrganizationSsoIdentifierRequired" xml:space="preserve">
|
||||||
<value>Organization SSO identifier required.</value>
|
<value>Organization SSO identifier required.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="AddTheKeyToAnExistingOrNewItem" xml:space="preserve">
|
||||||
|
<value>Add the key to an existing or new item</value>
|
||||||
|
</data>
|
||||||
|
<data name="ThereAreNoItemsInYourVaultThatMatchX" xml:space="preserve">
|
||||||
|
<value>There are no items in your vault that match "{0}"</value>
|
||||||
|
</data>
|
||||||
|
<data name="SearchForAnItemOrAddANewItem" xml:space="preserve">
|
||||||
|
<value>Search for an item or add a new item</value>
|
||||||
|
</data>
|
||||||
|
<data name="ThereAreNoItemsThatMatchTheSearch" xml:space="preserve">
|
||||||
|
<value>There are no items that match the search</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
30
src/App/Services/DeepLinkContext.cs
Normal file
30
src/App/Services/DeepLinkContext.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace Bit.App.Services
|
||||||
|
{
|
||||||
|
public class DeepLinkContext : IDeepLinkContext
|
||||||
|
{
|
||||||
|
public const string NEW_OTP_MESSAGE = "handleOTPUriMessage";
|
||||||
|
|
||||||
|
private readonly IMessagingService _messagingService;
|
||||||
|
|
||||||
|
public DeepLinkContext(IMessagingService messagingService)
|
||||||
|
{
|
||||||
|
_messagingService = messagingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnNewUri(Uri uri)
|
||||||
|
{
|
||||||
|
if (uri.Scheme == Constants.OtpAuthScheme)
|
||||||
|
{
|
||||||
|
_messagingService.Send(NEW_OTP_MESSAGE, uri.AbsoluteUri);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,6 @@ using Bit.Core.Abstractions;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Essentials;
|
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Utilities.AccountManagement
|
namespace Bit.App.Utilities.AccountManagement
|
||||||
@ -58,6 +57,13 @@ namespace Bit.App.Utilities.AccountManagement
|
|||||||
_broadcasterService.Subscribe(nameof(AccountsManager), OnMessage);
|
_broadcasterService.Subscribe(nameof(AccountsManager), OnMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction)
|
||||||
|
{
|
||||||
|
appOptionsAction(Options);
|
||||||
|
|
||||||
|
await NavigateOnAccountChangeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
|
public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
|
||||||
{
|
{
|
||||||
// TODO: this could be improved by doing chain of responsability pattern
|
// TODO: this could be improved by doing chain of responsability pattern
|
||||||
@ -89,6 +95,10 @@ namespace Bit.App.Utilities.AccountManagement
|
|||||||
{
|
{
|
||||||
_accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers);
|
_accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers);
|
||||||
}
|
}
|
||||||
|
else if (Options.OtpData != null)
|
||||||
|
{
|
||||||
|
_accountsManagerHost.Navigate(NavigationTarget.OtpCipherSelection);
|
||||||
|
}
|
||||||
else if (Options.CreateSend != null)
|
else if (Options.CreateSend != null)
|
||||||
{
|
{
|
||||||
_accountsManagerHost.Navigate(NavigationTarget.SendAddEdit);
|
_accountsManagerHost.Navigate(NavigationTarget.SendAddEdit);
|
||||||
|
@ -430,9 +430,11 @@ namespace Bit.App.Utilities
|
|||||||
Application.Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
|
Application.Current.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (appOptions.Uri != null)
|
if (appOptions.Uri != null
|
||||||
|
||
|
||||||
|
appOptions.OtpData != null)
|
||||||
{
|
{
|
||||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(appOptions));
|
Application.Current.MainPage = new NavigationPage(new CipherSelectionPage(appOptions));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (appOptions.CreateSend != null)
|
if (appOptions.CreateSend != null)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.App.Lists.ItemViewModels.CustomFields;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Lists.ItemViewModels.CustomFields;
|
||||||
|
using Bit.App.Services;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ namespace Bit.App.Utilities
|
|||||||
|
|
||||||
// TODO: This could be further improved by Lazy Registration since it may not be needed at all
|
// TODO: This could be further improved by Lazy Registration since it may not be needed at all
|
||||||
ServiceContainer.Register<ICustomFieldItemFactory>("customFieldItemFactory", new CustomFieldItemFactory(i18nService, eventService));
|
ServiceContainer.Register<ICustomFieldItemFactory>("customFieldItemFactory", new CustomFieldItemFactory(i18nService, eventService));
|
||||||
|
ServiceContainer.Register<IDeepLinkContext>(new DeepLinkContext(ServiceContainer.Resolve<IMessagingService>()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
/// which is used to handle Apple Watch state logic
|
/// which is used to handle Apple Watch state logic
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LastUserShouldConnectToWatchKey = "lastUserShouldConnectToWatch";
|
public const string LastUserShouldConnectToWatchKey = "lastUserShouldConnectToWatch";
|
||||||
|
public const string OtpAuthScheme = "otpauth";
|
||||||
public const string AppLocaleKey = "appLocale";
|
public const string AppLocaleKey = "appLocale";
|
||||||
public const string ClearSensitiveFields = "clearSensitiveFields";
|
public const string ClearSensitiveFields = "clearSensitiveFields";
|
||||||
public const int SelectFileRequestCode = 42;
|
public const int SelectFileRequestCode = 42;
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
Home,
|
Home,
|
||||||
AddEditCipher,
|
AddEditCipher,
|
||||||
AutofillCiphers,
|
AutofillCiphers,
|
||||||
SendAddEdit
|
SendAddEdit,
|
||||||
|
OtpCipherSelection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,39 +33,22 @@ namespace Bit.Core.Services
|
|||||||
var isSteamAuth = key?.ToLowerInvariant().StartsWith("steam://") ?? false;
|
var isSteamAuth = key?.ToLowerInvariant().StartsWith("steam://") ?? false;
|
||||||
if (isOtpAuth)
|
if (isOtpAuth)
|
||||||
{
|
{
|
||||||
var qsParams = CoreHelpers.GetQueryParams(key);
|
var otpData = new OtpData(key.ToLowerInvariant());
|
||||||
if (qsParams.ContainsKey("digits") && qsParams["digits"] != null &&
|
if (otpData.Digits > 0)
|
||||||
int.TryParse(qsParams["digits"].Trim(), out var digitParam))
|
|
||||||
{
|
{
|
||||||
if (digitParam > 10)
|
digits = Math.Min(otpData.Digits.Value, 10);
|
||||||
{
|
|
||||||
digits = 10;
|
|
||||||
}
|
|
||||||
else if (digitParam > 0)
|
|
||||||
{
|
|
||||||
digits = digitParam;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (qsParams.ContainsKey("period") && qsParams["period"] != null &&
|
if (otpData.Period.HasValue)
|
||||||
int.TryParse(qsParams["period"].Trim(), out var periodParam) && periodParam > 0)
|
|
||||||
{
|
{
|
||||||
period = periodParam;
|
period = otpData.Period.Value;
|
||||||
}
|
}
|
||||||
if (qsParams.ContainsKey("secret") && qsParams["secret"] != null)
|
if (otpData.Secret != null)
|
||||||
{
|
{
|
||||||
keyB32 = qsParams["secret"];
|
keyB32 = otpData.Secret;
|
||||||
}
|
}
|
||||||
if (qsParams.ContainsKey("algorithm") && qsParams["algorithm"] != null)
|
if (otpData.Algorithm.HasValue)
|
||||||
{
|
{
|
||||||
var algParam = qsParams["algorithm"].ToLowerInvariant();
|
alg = otpData.Algorithm.Value;
|
||||||
if (algParam == "sha256")
|
|
||||||
{
|
|
||||||
alg = CryptoHashAlgorithm.Sha256;
|
|
||||||
}
|
|
||||||
else if (algParam == "sha512")
|
|
||||||
{
|
|
||||||
alg = CryptoHashAlgorithm.Sha512;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (isSteamAuth)
|
else if (isSteamAuth)
|
||||||
|
@ -168,10 +168,27 @@ namespace Bit.Core.Utilities
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Uri.TryCreate(urlString, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Query))
|
if (Uri.TryCreate(urlString, UriKind.Absolute, out var uri))
|
||||||
|
{
|
||||||
|
return GetQueryParams(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
}
|
||||||
|
return new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, string> GetQueryParams(Uri uri)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(uri.Query))
|
||||||
{
|
{
|
||||||
return new Dictionary<string, string>();
|
return new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryStringNameValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
var queryStringNameValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||||
return queryStringNameValueCollection.AllKeys.Where(k => k != null).ToDictionary(k => k, k => queryStringNameValueCollection[k]);
|
return queryStringNameValueCollection.AllKeys.Where(k => k != null).ToDictionary(k => k, k => queryStringNameValueCollection[k]);
|
||||||
}
|
}
|
||||||
|
94
src/Core/Utilities/OtpData.cs
Normal file
94
src/Core/Utilities/OtpData.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities
|
||||||
|
{
|
||||||
|
public struct OtpData
|
||||||
|
{
|
||||||
|
const string LABEL_SEPARATOR = ":";
|
||||||
|
|
||||||
|
public OtpData(string absoluteUri)
|
||||||
|
{
|
||||||
|
if (!System.Uri.TryCreate(absoluteUri, UriKind.Absolute, out var uri)
|
||||||
|
||
|
||||||
|
uri.Scheme != Constants.OtpAuthScheme)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot create OtpData. Invalid OTP uri");
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri = absoluteUri;
|
||||||
|
AccountName = null;
|
||||||
|
Issuer = null;
|
||||||
|
Secret = null;
|
||||||
|
Digits = null;
|
||||||
|
Period = null;
|
||||||
|
Algorithm = null;
|
||||||
|
|
||||||
|
var escapedlabel = uri.Segments.Last();
|
||||||
|
if (escapedlabel != "/")
|
||||||
|
{
|
||||||
|
var label = UriExtensions.UnescapeDataString(escapedlabel);
|
||||||
|
if (label.Contains(LABEL_SEPARATOR))
|
||||||
|
{
|
||||||
|
var parts = label.Split(LABEL_SEPARATOR);
|
||||||
|
AccountName = parts[0].Trim();
|
||||||
|
Issuer = parts[1].Trim();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AccountName = label.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var qsParams = CoreHelpers.GetQueryParams(uri);
|
||||||
|
if (Issuer is null && qsParams.TryGetValue("issuer", out var issuer))
|
||||||
|
{
|
||||||
|
Issuer = issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qsParams.TryGetValue("secret", out var secret))
|
||||||
|
{
|
||||||
|
Secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qsParams.TryGetValue("digits", out var digitParam)
|
||||||
|
&&
|
||||||
|
int.TryParse(digitParam?.Trim(), out var digits))
|
||||||
|
{
|
||||||
|
Digits = digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qsParams.TryGetValue("period", out var periodParam)
|
||||||
|
&&
|
||||||
|
int.TryParse(periodParam?.Trim(), out var period)
|
||||||
|
&&
|
||||||
|
period > 0)
|
||||||
|
{
|
||||||
|
Period = period;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qsParams.TryGetValue("algorithm", out var algParam)
|
||||||
|
&&
|
||||||
|
algParam?.ToLower() is string alg)
|
||||||
|
{
|
||||||
|
if (alg == "sha256")
|
||||||
|
{
|
||||||
|
Algorithm = CryptoHashAlgorithm.Sha256;
|
||||||
|
}
|
||||||
|
else if (alg == "sha512")
|
||||||
|
{
|
||||||
|
Algorithm = CryptoHashAlgorithm.Sha512;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Uri { get; }
|
||||||
|
public string AccountName { get; }
|
||||||
|
public string Issuer { get; }
|
||||||
|
public string Secret { get; }
|
||||||
|
public int? Digits { get; }
|
||||||
|
public int? Period { get; }
|
||||||
|
public CryptoHashAlgorithm? Algorithm { get; }
|
||||||
|
}
|
||||||
|
}
|
18
src/Core/Utilities/UriExtensions.cs
Normal file
18
src/Core/Utilities/UriExtensions.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities
|
||||||
|
{
|
||||||
|
public static class UriExtensions
|
||||||
|
{
|
||||||
|
public static string UnescapeDataString(string uriString)
|
||||||
|
{
|
||||||
|
string unescapedUri;
|
||||||
|
while ((unescapedUri = System.Uri.UnescapeDataString(uriString)) != uriString)
|
||||||
|
{
|
||||||
|
uriString = unescapedUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unescapedUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -43,6 +43,8 @@ namespace Bit.iOS
|
|||||||
private IStateService _stateService;
|
private IStateService _stateService;
|
||||||
private IEventService _eventService;
|
private IEventService _eventService;
|
||||||
|
|
||||||
|
private LazyResolve<IDeepLinkContext> _deepLinkContext = new LazyResolve<IDeepLinkContext>();
|
||||||
|
|
||||||
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
|
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
|
||||||
{
|
{
|
||||||
Forms.Init();
|
Forms.Init();
|
||||||
@ -239,7 +241,7 @@ namespace Bit.iOS
|
|||||||
|
|
||||||
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
|
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
|
||||||
{
|
{
|
||||||
return Xamarin.Essentials.Platform.OpenUrl(app, url, options);
|
return _deepLinkContext.Value.OnNewUri(url) || Xamarin.Essentials.Platform.OpenUrl(app, url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity,
|
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity,
|
||||||
|
@ -29,6 +29,14 @@
|
|||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>com.8bit.bitwarden.url</string>
|
<string>com.8bit.bitwarden.url</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.8bit.bitwarden</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>otpauth</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
|
6
src/iOS/Resources/Assets.xcassets/Contents.json
Normal file
6
src/iOS/Resources/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
25
src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
vendored
Normal file
25
src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
vendored
Normal 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/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
vendored
Normal file
BIN
src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
vendored
Normal file
Binary file not shown.
BIN
src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf
vendored
Normal file
BIN
src/iOS/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf
vendored
Normal file
Binary file not shown.
@ -1,528 +1,608 @@
|
|||||||
{
|
{
|
||||||
"images": [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename": "ic_warning-1.pdf",
|
"filename" : "ic_warning-1.pdf",
|
||||||
"idiom": "universal"
|
"idiom" : "universal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"scale": "1x",
|
"appearances" : [
|
||||||
"idiom": "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "3x",
|
|
||||||
"idiom": "universal"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "1x",
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"subtype": "retina4",
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "3x",
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "ipad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "1x",
|
|
||||||
"idiom": "ipad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "ipad"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "watch"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "watch"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"screenWidth": "{130,145}",
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "watch"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"screenWidth": "{146,165}",
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "watch"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "mac"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "1x",
|
|
||||||
"idiom": "mac"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "mac"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scale": "3x",
|
|
||||||
"idiom": "car"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"appearances": [
|
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "universal"
|
"idiom" : "universal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "1x",
|
"idiom" : "universal"
|
||||||
"idiom": "universal"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "universal",
|
||||||
{
|
"scale" : "1x"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "universal"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "3x",
|
"idiom" : "universal",
|
||||||
"idiom": "universal"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "iphone"
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "universal",
|
||||||
{
|
"scale" : "2x"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "1x",
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "universal",
|
||||||
"idiom": "iphone"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"subtype": "retina4",
|
"appearances" : [
|
||||||
"appearances": [
|
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "universal",
|
||||||
"idiom": "iphone"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "universal",
|
||||||
{
|
"scale" : "3x"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "3x",
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "ipad"
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "1x",
|
"idiom" : "universal",
|
||||||
"idiom": "ipad"
|
"scale" : "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "iphone"
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "ipad"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "watch"
|
"idiom" : "iphone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "iphone"
|
||||||
"idiom": "watch"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"screenWidth": "{130,145}",
|
"idiom" : "iphone",
|
||||||
"appearances": [
|
"scale" : "1x"
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "watch"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"screenWidth": "{146,165}",
|
"appearances" : [
|
||||||
"appearances": [
|
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "iphone",
|
||||||
"idiom": "watch"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "mac"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "iphone",
|
||||||
{
|
"scale" : "2x"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "1x",
|
|
||||||
"idiom": "mac"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "iphone",
|
||||||
"idiom": "mac"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "car"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "iphone",
|
||||||
{
|
"scale" : "3x"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "dark"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "car"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "dark"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "3x",
|
"idiom" : "iphone",
|
||||||
"idiom": "car"
|
"scale" : "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "universal"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "iphone",
|
||||||
{
|
"scale" : "1x",
|
||||||
"appearance": "luminosity",
|
"subtype" : "retina4"
|
||||||
"value": "light"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "1x",
|
|
||||||
"idiom": "universal"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "iphone",
|
||||||
"idiom": "universal"
|
"scale" : "1x",
|
||||||
|
"subtype" : "retina4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "3x",
|
"idiom" : "iphone",
|
||||||
"idiom": "universal"
|
"scale" : "1x",
|
||||||
|
"subtype" : "retina4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "iphone",
|
||||||
{
|
"scale" : "2x",
|
||||||
"appearance": "luminosity",
|
"subtype" : "retina4"
|
||||||
"value": "light"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "1x",
|
"idiom" : "iphone",
|
||||||
"idiom": "iphone"
|
"scale" : "2x",
|
||||||
|
"subtype" : "retina4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "iphone",
|
||||||
"idiom": "iphone"
|
"scale" : "2x",
|
||||||
|
"subtype" : "retina4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"subtype": "retina4",
|
"idiom" : "iphone",
|
||||||
"appearances": [
|
"scale" : "3x",
|
||||||
{
|
"subtype" : "retina4"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "light"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "iphone"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "3x",
|
"idiom" : "iphone",
|
||||||
"idiom": "iphone"
|
"scale" : "3x",
|
||||||
|
"subtype" : "retina4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "ipad"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"subtype" : "retina4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "ipad"
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "light"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "1x",
|
|
||||||
"idiom": "ipad"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "ipad"
|
||||||
"idiom": "ipad"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idiom": "watch"
|
"idiom" : "ipad"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "ipad",
|
||||||
{
|
"scale" : "1x"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "light"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"scale": "2x",
|
|
||||||
"idiom": "watch"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"screenWidth": "{130,145}",
|
"appearances" : [
|
||||||
"appearances": [
|
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "ipad",
|
||||||
"idiom": "watch"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"screenWidth": "{146,165}",
|
"appearances" : [
|
||||||
"appearances": [
|
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "ipad",
|
||||||
"idiom": "watch"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "ipad",
|
||||||
{
|
"scale" : "2x"
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "light"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom": "mac"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "1x",
|
"idiom" : "ipad",
|
||||||
"idiom": "mac"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "ipad",
|
||||||
"idiom": "mac"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"idiom" : "car"
|
||||||
{
|
|
||||||
"appearance": "luminosity",
|
|
||||||
"value": "light"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idiom": "car"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "light"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "2x",
|
"idiom" : "car"
|
||||||
"idiom": "car"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"appearances": [
|
"appearances" : [
|
||||||
{
|
{
|
||||||
"appearance": "luminosity",
|
"appearance" : "luminosity",
|
||||||
"value": "light"
|
"value" : "dark"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scale": "3x",
|
"idiom" : "car"
|
||||||
"idiom": "car"
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "car",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "car",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "car",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "car",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "car",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "car",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "mac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "mac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"screen-width" : ">161"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"screen-width" : ">145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">161"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "light"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">145"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info": {
|
"info" : {
|
||||||
"version": 1,
|
"author" : "xcode",
|
||||||
"author": "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -179,6 +179,10 @@
|
|||||||
<BundleResource Include="Resources\generate.png" />
|
<BundleResource Include="Resources\generate.png" />
|
||||||
<BundleResource Include="Resources\generate%402x.png" />
|
<BundleResource Include="Resources\generate%402x.png" />
|
||||||
<BundleResource Include="Resources\generate%403x.png" />
|
<BundleResource Include="Resources\generate%403x.png" />
|
||||||
|
<ImageAsset Include="Resources\Assets.xcassets\Contents.json" />
|
||||||
|
<ImageAsset Include="Resources\Assets.xcassets\empty_items_state.imageset\Empty-items-state-dark.pdf" />
|
||||||
|
<ImageAsset Include="Resources\Assets.xcassets\empty_items_state.imageset\Empty-items-state.pdf" />
|
||||||
|
<ImageAsset Include="Resources\Assets.xcassets\empty_items_state.imageset\Contents.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InterfaceDefinition Include="LaunchScreen.storyboard" />
|
<InterfaceDefinition Include="LaunchScreen.storyboard" />
|
||||||
|
Loading…
Reference in New Issue
Block a user