From baf77eb3a3a8b7a657269946639d3579c44a3678 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 26 Apr 2019 00:26:09 -0400 Subject: [PATCH] view page login info with copy buttons --- src/App/Controls/FaButton.cs | 21 ++++ src/App/Pages/Vault/ViewPage.xaml | 109 +++++++++++++++++- src/App/Pages/Vault/ViewPage.xaml.cs | 1 + src/App/Pages/Vault/ViewPageViewModel.cs | 140 ++++++++++++++++++++++- src/App/Styles/Android.xaml | 11 ++ src/App/Styles/Base.xaml | 57 +++++++++ src/App/Styles/Dark.xaml | 5 + src/App/Styles/Light.xaml | 5 + src/Core/Utilities/ExtendedViewModel.cs | 9 +- 9 files changed, 350 insertions(+), 8 deletions(-) create mode 100644 src/App/Controls/FaButton.cs diff --git a/src/App/Controls/FaButton.cs b/src/App/Controls/FaButton.cs new file mode 100644 index 000000000..88289616b --- /dev/null +++ b/src/App/Controls/FaButton.cs @@ -0,0 +1,21 @@ +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class FaButton : Button + { + public FaButton() + { + Padding = 0; + switch(Device.RuntimePlatform) + { + case Device.iOS: + FontFamily = "FontAwesome"; + break; + case Device.Android: + FontFamily = "FontAwesome.ttf#FontAwesome"; + break; + } + } + } +} diff --git a/src/App/Pages/Vault/ViewPage.xaml b/src/App/Pages/Vault/ViewPage.xaml index bf8083c67..0d398768b 100644 --- a/src/App/Pages/Vault/ViewPage.xaml +++ b/src/App/Pages/Vault/ViewPage.xaml @@ -5,6 +5,7 @@ x:Class="Bit.App.Pages.ViewPage" xmlns:pages="clr-namespace:Bit.App.Pages" xmlns:u="clr-namespace:Bit.App.Utilities" + xmlns:controls="clr-namespace:Bit.App.Controls" x:DataType="pages:ViewPageViewModel" Title="{Binding PageTitle}"> @@ -17,19 +18,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Vault/ViewPage.xaml.cs b/src/App/Pages/Vault/ViewPage.xaml.cs index 711cd4101..90b433ae7 100644 --- a/src/App/Pages/Vault/ViewPage.xaml.cs +++ b/src/App/Pages/Vault/ViewPage.xaml.cs @@ -49,6 +49,7 @@ namespace Bit.App.Pages { base.OnDisappearing(); _broadcasterService.Unsubscribe(nameof(ViewPage)); + _vm.CleanUp(); } } } diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index 5a1716ea3..aac739e1b 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -3,7 +3,9 @@ using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; +using System; using System.Threading.Tasks; +using Xamarin.Forms; namespace Bit.App.Pages { @@ -12,39 +14,167 @@ namespace Bit.App.Pages private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; private readonly IUserService _userService; + private readonly ITotpService _totpService; + private readonly IPlatformUtilsService _platformUtilsService; private CipherView _cipher; private bool _canAccessPremium; + private string _totpCode; + private string _totpCodeFormatted; + private string _totpSec; + private bool _totpLow; + private DateTime? _totpInterval = null; public ViewPageViewModel() { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _cipherService = ServiceContainer.Resolve("cipherService"); _userService = ServiceContainer.Resolve("userService"); + _totpService = ServiceContainer.Resolve("totpService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + CopyCommand = new Command(CopyAsync); PageTitle = AppResources.ViewItem; } - public string CipherId { get; set; } + public Command CopyCommand { get; set; } + public string CipherId { get; set; } public CipherView Cipher { get => _cipher; - set => SetProperty(ref _cipher, value); + set => SetProperty(ref _cipher, value, + additionalPropertyNames: new string[] + { + nameof(IsLogin), + nameof(IsIdentity), + nameof(IsCard), + nameof(IsSecureNote) + }); } public bool CanAccessPremium { get => _canAccessPremium; set => SetProperty(ref _canAccessPremium, value); } + public bool IsLogin => _cipher?.Type == Core.Enums.CipherType.Login; + public bool IsIdentity => _cipher?.Type == Core.Enums.CipherType.Identity; + public bool IsCard => _cipher?.Type == Core.Enums.CipherType.Card; + public bool IsSecureNote => _cipher?.Type == Core.Enums.CipherType.SecureNote; + public string TotpCodeFormatted + { + get => _totpCodeFormatted; + set => SetProperty(ref _totpCodeFormatted, value); + } + public string TotpSec + { + get => _totpSec; + set => SetProperty(ref _totpSec, value); + } + public bool TotpLow + { + get => _totpLow; + set => SetProperty(ref _totpLow, value); + } public async Task LoadAsync() { - // TODO: Cleanup - + CleanUp(); var cipher = await _cipherService.GetAsync(CipherId); Cipher = await cipher.DecryptAsync(); CanAccessPremium = await _userService.CanAccessPremiumAsync(); - // TODO: Totp + if(Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) && + (Cipher.OrganizationUseTotp || CanAccessPremium)) + { + await TotpUpdateCodeAsync(); + var interval = _totpService.GetTimeInterval(Cipher.Login.Totp); + await TotpTickAsync(interval); + _totpInterval = DateTime.UtcNow; + Device.StartTimer(new TimeSpan(0, 0, 1), () => + { + if(_totpInterval == null) + { + return false; + } + var task = TotpTickAsync(interval); + return true; + }); + } + } + + public void CleanUp() + { + _totpInterval = null; + } + + private async Task TotpUpdateCodeAsync() + { + if(Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null) + { + _totpInterval = null; + return; + } + _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; + _totpInterval = null; + } + } + + private async Task TotpTickAsync(int intervalSeconds) + { + var epoc = CoreHelpers.EpocUtcNow() / 1000; + var mod = epoc % intervalSeconds; + var totpSec = intervalSeconds - mod; + TotpSec = totpSec.ToString(); + TotpLow = totpSec < 7; + if(mod == 0) + { + await TotpUpdateCodeAsync(); + } + } + + private async void CopyAsync(string id) + { + string text = null; + string name = null; + if(id == "LoginUsername") + { + text = Cipher.Login.Username; + name = AppResources.Username; + } + else if(id == "LoginPassword") + { + text = Cipher.Login.Password; + name = AppResources.Password; + } + else if(id == "LoginTotp") + { + text = _totpCode; + name = AppResources.VerificationCodeTotp; + } + + if(text != null) + { + await _platformUtilsService.CopyToClipboardAsync(text); + if(!string.IsNullOrWhiteSpace(name)) + { + _platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, name)); + } + } } } } diff --git a/src/App/Styles/Android.xaml b/src/App/Styles/Android.xaml index 661375a15..adc9ec69a 100644 --- a/src/App/Styles/Android.xaml +++ b/src/App/Styles/Android.xaml @@ -22,4 +22,15 @@ + + + + diff --git a/src/App/Styles/Base.xaml b/src/App/Styles/Base.xaml index 12a0155aa..d8d7f94a4 100644 --- a/src/App/Styles/Base.xaml +++ b/src/App/Styles/Base.xaml @@ -62,4 +62,61 @@ + + + + + + + + + + diff --git a/src/App/Styles/Dark.xaml b/src/App/Styles/Dark.xaml index 6382d5b75..0010370aa 100644 --- a/src/App/Styles/Dark.xaml +++ b/src/App/Styles/Dark.xaml @@ -10,5 +10,10 @@ #bf7e16 #777777 + #dddddd + + #f0f0f0 + + #f0f0f0 #3c8dbc diff --git a/src/App/Styles/Light.xaml b/src/App/Styles/Light.xaml index f9ee1effb..2ad1b3324 100644 --- a/src/App/Styles/Light.xaml +++ b/src/App/Styles/Light.xaml @@ -10,5 +10,10 @@ #bf7e16 #777777 + #dddddd + + #dddddd + + #f0f0f0 #3c8dbc diff --git a/src/Core/Utilities/ExtendedViewModel.cs b/src/Core/Utilities/ExtendedViewModel.cs index 62ac8df1a..1e8708477 100644 --- a/src/Core/Utilities/ExtendedViewModel.cs +++ b/src/Core/Utilities/ExtendedViewModel.cs @@ -10,7 +10,7 @@ namespace Bit.Core.Utilities public event PropertyChangedEventHandler PropertyChanged; protected bool SetProperty(ref T backingStore, T value, Action onChanged = null, - [CallerMemberName]string propertyName = "") + [CallerMemberName]string propertyName = "", string[] additionalPropertyNames = null) { if(EqualityComparer.Default.Equals(backingStore, value)) { @@ -19,6 +19,13 @@ namespace Bit.Core.Utilities backingStore = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + if(PropertyChanged != null && additionalPropertyNames != null) + { + foreach(var prop in additionalPropertyNames) + { + PropertyChanged.Invoke(this, new PropertyChangedEventArgs(prop)); + } + } onChanged?.Invoke(); return true; }