view page login info with copy buttons

This commit is contained in:
Kyle Spearrin 2019-04-26 00:26:09 -04:00
parent 0625f313dc
commit baf77eb3a3
9 changed files with 350 additions and 8 deletions

View file

@ -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;
}
}
}
}

View file

@ -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}">
<ContentPage.BindingContext>
@ -17,6 +18,10 @@
</ContentPage.ToolbarItems>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n ItemInformation}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row">
<Label
Text="{u:I18n Name}"
@ -24,6 +29,106 @@
<Label
Text="{Binding Cipher.Name, Mode=OneWay}"
StyleClass="box-value" />
</StackLayout>
<BoxView StyleClass="box-row-separator" />
<StackLayout IsVisible="{Binding IsLogin}">
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n Username}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding Cipher.Login.Username, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<controls:FaButton StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="LoginUsername"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator" />
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n Password}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding Cipher.Login.MaskedPassword, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<controls:FaButton StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="LoginPassword"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2" />
</Grid>
<BoxView StyleClass="box-row-separator" />
<Grid StyleClass="box-row">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Text="{u:I18n VerificationCodeTotp}"
StyleClass="box-label"
Grid.Row="0"
Grid.Column="0" />
<Label
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
StyleClass="box-value"
Grid.Row="1"
Grid.Column="0" />
<Label
Text="{Binding TotpSec, Mode=OneWay}"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
HorizontalOptions="End"
VerticalOptions="CenterAndExpand"/>
<controls:FaButton StyleClass="box-row-button, box-row-button-platform"
Text="&#xf0ea;"
Command="{Binding CopyCommand}"
CommandParameter="LoginTotp"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2" />
</Grid>
</StackLayout>
<StackLayout IsVisible="{Binding IsCard}">
</StackLayout>
<StackLayout IsVisible="{Binding IsIdentity}">
</StackLayout>
<StackLayout StyleClass="box-row">
<Label

View file

@ -49,6 +49,7 @@ namespace Bit.App.Pages
{
base.OnDisappearing();
_broadcasterService.Unsubscribe(nameof(ViewPage));
_vm.CleanUp();
}
}
}

View file

@ -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<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_userService = ServiceContainer.Resolve<IUserService>("userService");
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
CopyCommand = new Command<string>(CopyAsync);
PageTitle = AppResources.ViewItem;
}
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));
}
}
}
}
}

View file

@ -22,4 +22,15 @@
<Style TargetType="Grid"
Class="list-row-platform">
</Style>
<!-- Box -->
<Style TargetType="Button"
ApplyToDerivedTypes="True"
Class="box-row-button-platform">
<Setter Property="WidthRequest"
Value="40" />
<Setter Property="FontSize"
Value="28" />
</Style>
</ResourceDictionary>

View file

@ -62,4 +62,61 @@
<Setter Property="TextColor"
Value="{StaticResource MutedColor}" />
</Style>
<!-- Box -->
<Style TargetType="StackLayout"
Class="box">
<Setter Property="Padding"
Value="10, 0" />
<Setter Property="Spacing"
Value="0" />
</Style>
<Style TargetType="Grid"
Class="box-row">
<Setter Property="Padding"
Value="0, 10" />
<Setter Property="RowSpacing"
Value="0" />
<Setter Property="ColumnSpacing"
Value="10" />
</Style>
<Style TargetType="StackLayout"
Class="box-row">
<Setter Property="Padding"
Value="0, 10" />
<Setter Property="Spacing"
Value="0" />
</Style>
<Style TargetType="Button"
ApplyToDerivedTypes="True"
Class="box-row-button">
<Setter Property="BackgroundColor"
Value="Transparent" />
<Setter Property="Padding"
Value="0" />
<Setter Property="TextColor"
Value="{StaticResource PrimaryColor}" />
<Setter Property="HorizontalOptions"
Value="End" />
<Setter Property="VerticalOptions"
Value="CenterAndExpand" />
</Style>
<Style TargetType="BoxView"
Class="box-row-separator">
<Setter Property="HeightRequest"
Value="1" />
<Setter Property="Color"
Value="{StaticResource BoxBorderColor}" />
</Style>
<Style TargetType="Label"
Class="box-label">
<Setter Property="FontSize"
Value="Small" />
<Setter Property="TextColor"
Value="{StaticResource MutedColor}" />
</Style>
<Style TargetType="Label"
Class="box-value">
</Style>
</ResourceDictionary>

View file

@ -10,5 +10,10 @@
<Color x:Key="WarningColor">#bf7e16</Color>
<Color x:Key="MutedColor">#777777</Color>
<Color x:Key="BorderColor">#dddddd</Color>
<Color x:Key="BoxBorderColor">#f0f0f0</Color>
<Color x:Key="ListItemBorderColor">#f0f0f0</Color>
<Color x:Key="ListHeaderTextColor">#3c8dbc</Color>
</ResourceDictionary>

View file

@ -10,5 +10,10 @@
<Color x:Key="WarningColor">#bf7e16</Color>
<Color x:Key="MutedColor">#777777</Color>
<Color x:Key="BorderColor">#dddddd</Color>
<Color x:Key="BoxBorderColor">#dddddd</Color>
<Color x:Key="ListItemBorderColor">#f0f0f0</Color>
<Color x:Key="ListHeaderTextColor">#3c8dbc</Color>
</ResourceDictionary>

View file

@ -10,7 +10,7 @@ namespace Bit.Core.Utilities
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T backingStore, T value, Action onChanged = null,
[CallerMemberName]string propertyName = "")
[CallerMemberName]string propertyName = "", string[] additionalPropertyNames = null)
{
if(EqualityComparer<T>.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;
}