From 5e2142fba74ec0212cc4af05f12d63fa678711da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Bispo?= Date: Tue, 7 Jun 2022 18:38:48 +0100 Subject: [PATCH] PS-70 Added toggle to quickly filter TOTP cypher items and show their details, Added new text resource --- .../AuthenticatorViewCell.xaml | 2 +- .../Vault/GroupingsPage/GroupingsPage.xaml | 32 +++++ .../Vault/GroupingsPage/GroupingsPage.xaml.cs | 5 + .../GroupingsPage/GroupingsPageListGroup.cs | 6 +- .../GroupingsPageListItemSelector.cs | 7 + .../GroupingsPage/GroupingsPageOTPListItem.cs | 129 ++++++++++++++++++ .../GroupingsPage/GroupingsPageViewModel.cs | 59 +++++++- src/App/Resources/AppResources.Designer.cs | 6 + src/App/Resources/AppResources.resx | 3 + 9 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 src/App/Pages/Vault/GroupingsPage/GroupingsPageOTPListItem.cs diff --git a/src/App/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml b/src/App/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml index 2c5a31042..0b9f0a28e 100644 --- a/src/App/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml +++ b/src/App/Controls/AuthenticatorViewCell/AuthenticatorViewCell.xaml @@ -10,7 +10,7 @@ StyleClass="list-row, list-row-platform" RowSpacing="0" ColumnSpacing="0" - x:DataType="pages:AuthenticatorPageListItem"> + x:DataType="pages:GroupingsPageTOTPListItem"> diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml index 7df644850..d591b26ac 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml @@ -53,6 +53,14 @@ WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" /> + + + + @@ -131,6 +140,29 @@ AutomationProperties.Name="{u:I18n Filter}" /> + + + + public class GroupingsPageListGroup : List { public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false) - : this(new List(), name, count, doUpper, first) + : this(new List(), name, count, doUpper, first) { } - public GroupingsPageListGroup(List groupItems, string name, int count, + public GroupingsPageListGroup(IEnumerable groupItems, string name, int count, bool doUpper = true, bool first = false) { AddRange(groupItems); diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs index a2e2207b4..d05a1cfb6 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs @@ -7,6 +7,7 @@ namespace Bit.App.Pages public DataTemplate HeaderTemplate { get; set; } public DataTemplate CipherTemplate { get; set; } public DataTemplate GroupTemplate { get; set; } + public DataTemplate AuthenticatorTemplate { get; set; } protected override DataTemplate OnSelectTemplate(object item, BindableObject container) { @@ -15,10 +16,16 @@ namespace Bit.App.Pages return HeaderTemplate; } + if (item is GroupingsPageTOTPListItem) + { + return AuthenticatorTemplate; + } + if (item is GroupingsPageListItem listItem) { return listItem.Cipher != null ? CipherTemplate : GroupTemplate; } + return null; } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageOTPListItem.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageOTPListItem.cs new file mode 100644 index 000000000..d9e3e63c9 --- /dev/null +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageOTPListItem.cs @@ -0,0 +1,129 @@ +using System; +using System.Threading.Tasks; +using Bit.App.Resources; +using Bit.App.Utilities; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class GroupingsPageTOTPListItem : ExtendedViewModel, IGroupingsPageListItem + { + //private string _totpCode; + private readonly ITotpService _totpService; + + //public CipherView Cipher { get; set; } + //public CipherType? Type { get; set; } + //public int interval { get; set; } + //public long TotpSec { get; set; } + //private DateTime? _totpInterval = null; + + private CipherView _cipher; + + private bool _websiteIconsEnabled; + private string _iconImageSource = string.Empty; + + public int interval { get; set; } + private string _totpSec; + + private string _totpCode; + private string _totpCodeFormatted = "938 928"; + + + public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled) + { + _totpService = ServiceContainer.Resolve("totpService"); + + Cipher = cipherView; + WebsiteIconsEnabled = websiteIconsEnabled; + interval = _totpService.GetTimeInterval(Cipher.Login.Totp); + } + + + public Command CopyCommand { get; set; } + + public CipherView Cipher + { + get => _cipher; + set => SetProperty(ref _cipher, value); + } + + public string TotpCodeFormatted + { + get => _totpCodeFormatted; + set => SetProperty(ref _totpCodeFormatted, value); + } + + public string TotpSec + { + get => _totpSec; + set => SetProperty(ref _totpSec, value); + } + + public bool WebsiteIconsEnabled + { + get => _websiteIconsEnabled; + set => SetProperty(ref _websiteIconsEnabled, value); + } + + public bool ShowIconImage + { + get => WebsiteIconsEnabled + && !string.IsNullOrWhiteSpace(Cipher.Login?.Uri) + && IconImageSource != null; + } + + public string IconImageSource + { + get + { + if (_iconImageSource == string.Empty) // default value since icon source can return null + { + _iconImageSource = IconImageHelper.GetLoginIconImage(Cipher); + } + return _iconImageSource; + } + + } + + public async Task TotpTickAsync() + { + var epoc = CoreHelpers.EpocUtcNow() / 1000; + var mod = epoc % interval; + var totpSec = interval - mod; + TotpSec = totpSec.ToString(); + //TotpLow = totpSec < 7; + if (mod == 0) + { + await TotpUpdateCodeAsync(); + } + + } + + public async Task TotpUpdateCodeAsync() + { + _totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp); + if (_totpCode != null) + { + if (_totpCode.Length > 4) + { + var half = (int)Math.Floor(_totpCode.Length / 2M); + TotpCodeFormatted = string.Format("{0} {1}", _totpCode.Substring(0, half), + _totpCode.Substring(half)); + } + else + { + TotpCodeFormatted = _totpCode; + } + } + else + { + TotpCodeFormatted = null; + } + } + } +} diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs index 94027f5c3..32016bb5a 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs @@ -31,6 +31,8 @@ namespace Bit.App.Pages private bool _websiteIconsEnabled; private bool _syncRefreshing; private bool _showVaultFilter; + private bool _showTOTPFilter; + private bool _totpFilterEnable; private string _vaultFilterSelection; private string _noDataText; private List _organizations; @@ -81,6 +83,9 @@ namespace Bit.App.Pages VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); + TOTPFilterCommand = new AsyncCommand(TOTPFilterAsync, + onException: ex => _logger.Exception(ex), + allowsMultipleExecutions: false); AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) { @@ -158,6 +163,17 @@ namespace Bit.App.Pages get => _showVaultFilter; set => SetProperty(ref _showVaultFilter, value); } + public bool ShowTOTPFilter + { + get => _showTOTPFilter; + set => SetProperty(ref _showTOTPFilter, value); + } + public bool TOTPFilterEnable + { + get => _totpFilterEnable; + set => SetProperty(ref _totpFilterEnable, value); + } + public string VaultFilterDescription { get @@ -177,6 +193,7 @@ namespace Bit.App.Pages public Command RefreshCommand { get; set; } public Command CipherOptionsCommand { get; set; } public ICommand VaultFilterCommand { get; } + public ICommand TOTPFilterCommand { get; } public bool LoadedOnce { get; set; } public async Task LoadAsync() @@ -211,13 +228,14 @@ namespace Bit.App.Pages } PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault; } - _doingLoad = true; LoadedOnce = true; ShowNoData = false; Loading = true; ShowList = false; ShowAddCipherButton = !Deleted; + ShowTOTPFilter = Type == CipherType.Login; + var groupedItems = new List(); var page = Page as GroupingsPage; @@ -297,10 +315,36 @@ namespace Bit.App.Pages } if (Ciphers?.Any() ?? false) { - var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted) - .Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); - groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, - ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any())); + if (TOTPFilterEnable) + { + var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted && !string.IsNullOrEmpty(c.Login.Totp)) + .Select(c => new GroupingsPageTOTPListItem(c, true)).ToList(); + groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, + ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any())); + + foreach (var item in ciphersListItems) + { + item.TotpUpdateCodeAsync(); + } + Device.StartTimer(new TimeSpan(0, 0, 1), () => + { + if (Type != CipherType.Login) + return false; + + foreach (var item in ciphersListItems) + { + item.TotpTickAsync(); + } + return true; + }); + } + else + { + var ciphersListItems = Ciphers.Where(c => c.IsDeleted == Deleted) + .Select(c => new GroupingsPageListItem { Cipher = c }).ToList(); + groupedItems.Add(new GroupingsPageListGroup(ciphersListItems, AppResources.Items, + ciphersListItems.Count, uppercaseGroupNames, !MainPage && !groupedItems.Any())); + } } if (ShowNoFolderCipherGroup) { @@ -413,6 +457,11 @@ namespace Bit.App.Pages await LoadAsync(); } + public async Task TOTPFilterAsync() + { + await LoadAsync(); + } + public async Task SelectCipherAsync(CipherView cipher) { var page = new ViewPage(cipher.Id); diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index a8a6d45fa..aa9dc4771 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -4024,5 +4024,11 @@ namespace Bit.App.Resources { return ResourceManager.GetString("All", resourceCulture); } } + + public static string DisplayItemsContainingTOTP { + get { + return ResourceManager.GetString("DisplayItemsContainingTOTP", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index cd18fc3ae..859c26b91 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2248,4 +2248,7 @@ All + + Display items containing TOTP +