1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-09-28 03:57:43 +02:00

[SSG-416] Mobile PR Fixes

This commit is contained in:
André Bispo 2022-07-12 23:00:31 +01:00
parent 8ed909eb91
commit 7cf34b845e
21 changed files with 507 additions and 404 deletions

View File

@ -9,30 +9,23 @@
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
StyleClass="list-row, list-row-platform"
HorizontalOptions="FillAndExpand"
x:DataType="pages:GroupingsPageTOTPListItem">
x:DataType="pages:GroupingsPageTOTPListItem"
ColumnDefinitions="40,*,40,Auto,40"
RowSpacing="0"
Padding="0,10,0,0"
RowDefinitions="Auto,Auto">
<Grid.Resources>
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
<u:InverseBoolConverter x:Key="inverseBool" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<controls:IconLabel
Grid.Column="0"
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage, Converter={StaticResource inverseBool}}"
Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
AutomationProperties.IsInAccessibleTree="False" />
@ -46,47 +39,35 @@
VerticalOptions="Center"
WidthRequest="22"
HeightRequest="22"
Grid.RowSpan="2"
IsVisible="{Binding ShowIconImage}"
Source="{Binding IconImageSource, Mode=OneTime}"
AutomationProperties.IsInAccessibleTree="False" />
<Grid
RowSpacing="0"
ColumnSpacing="0"
Grid.Row="0"
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
VerticalOptions="Center"
HorizontalOptions="FillAndExpand"
Padding="0, 15, 15, 10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
Grid.Row="0"
VerticalTextAlignment="End"
VerticalOptions="End"
StyleClass="list-title, list-title-platform"
Text="{Binding Cipher.Name}" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="0"
StyleClass="list-title, list-title-platform"
Text="{Binding Cipher.Name}" />
<Label
LineBreakMode="TailTruncation"
Grid.Column="0"
Grid.Row="1"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Cipher.SubTitle}" />
</Grid>
<Label
LineBreakMode="TailTruncation"
Grid.Column="1"
Grid.Row="1"
VerticalTextAlignment="Start"
VerticalOptions="Start"
StyleClass="list-subtitle, list-subtitle-platform"
Text="{Binding Cipher.SubTitle}" />
<controls:CircularProgressbarView
Progress="{Binding Progress}"
ProgressColor="{StaticResource PrimaryColor}"
EndingProgressColor="{StaticResource DangerColor}"
BackgroundProgressColor="{StaticResource BackgroundColor}"
StrokeWidth="3"
Radius="15"
Grid.Row="0"
Grid.Column="2"
HorizontalOptions="FillAndExpand"
Grid.RowSpan="2"
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" />
<Label
@ -94,20 +75,22 @@
Style="{DynamicResource textTotp}"
Grid.Row="0"
Grid.Column="2"
Grid.RowSpan="2"
StyleClass="text-sm"
XAlign="Center"
YAlign="Center"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand" />
HorizontalTextAlignment="Center"
HorizontalOptions="Fill"
VerticalTextAlignment="Center"
VerticalOptions="Fill" />
<StackLayout
Grid.Row="0"
Grid.Column="3"
Margin="3,0,2,0"
Spacing="5"
Grid.RowSpan="2"
Orientation="Horizontal"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
HorizontalOptions="Fill"
VerticalOptions="Fill">
<controls:MonoLabel
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
@ -135,6 +118,7 @@
CommandParameter="LoginTotp"
Grid.Row="0"
Grid.Column="4"
Grid.RowSpan="2"
Padding="0,0,1,0"
HorizontalOptions="Center"
VerticalOptions="Center"

View File

@ -63,12 +63,5 @@ namespace Bit.App.Controls
}
}
private string _totpCodeFormatted = "938 928";
public string TotpCodeFormatted
{
get => _totpCodeFormatted;
set => _totpCodeFormatted = value;
}
}
}

View File

@ -0,0 +1,139 @@
using System;
using System.Runtime.CompilerServices;
using SkiaSharp;
using SkiaSharp.Views.Forms;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class CircularProgressbarView : SKCanvasView
{
private Circle _circle;
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
nameof(Progress), typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
public static readonly BindableProperty RadiusProperty = BindableProperty.Create(
nameof(Radius), typeof(float), typeof(CircularProgressbarView), 15f);
public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create(
nameof(StrokeWidth), typeof(float), typeof(CircularProgressbarView), 3f);
public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create(
nameof(ProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public static readonly BindableProperty EndingProgressColorProperty = BindableProperty.Create(
nameof(EndingProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public static readonly BindableProperty BackgroundProgressColorProperty = BindableProperty.Create(
nameof(BackgroundProgressColor), typeof(Color), typeof(CircularProgressbarView), Color.Default);
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
public float Radius
{
get => (float)GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
public float StrokeWidth
{
get => (float)GetValue(StrokeWidthProperty);
set => SetValue(StrokeWidthProperty, value);
}
public Color ProgressColor
{
get => (Color)GetValue(ProgressColorProperty);
set => SetValue(ProgressColorProperty, value);
}
public Color EndingProgressColor
{
get => (Color)GetValue(EndingProgressColorProperty);
set => SetValue(EndingProgressColorProperty, value);
}
public Color BackgroundProgressColor
{
get => (Color)GetValue(BackgroundProgressColorProperty);
set => SetValue(BackgroundProgressColorProperty, value);
}
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var context = bindable as CircularProgressbarView;
context.InvalidateSurface();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(Progress))
{
_circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
}
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
if(_circle != null)
{
_circle.CalculateCenter(e.Info);
e.Surface.Canvas.Clear();
DrawCircle(e.Surface.Canvas, _circle, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor());
DrawArc(e.Surface.Canvas, _circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
}
}
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
{
canvas.DrawCircle(circle.Center, circle.Redius,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = color,
IsStroke = true,
IsAntialias = true
});
}
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
{
var progressValue = progress.Invoke();
var angle = progressValue * 3.6f;
canvas.DrawArc(circle.Rect, 270, angle, false,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = progressValue < 20f ? progressEndColor : color,
IsStroke = true,
IsAntialias = true
});
}
}
public class Circle
{
private readonly Func<SKImageInfo, SKPoint> _centerFunc;
public Circle(float redius, Func<SKImageInfo, SKPoint> centerFunc)
{
_centerFunc = centerFunc;
Redius = redius;
}
public SKPoint Center { get; set; }
public float Redius { get; set; }
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
public void CalculateCenter(SKImageInfo argsInfo)
{
Center = _centerFunc(argsInfo);
}
}
}

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:forms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="Bit.App.Controls.CircularProgressbarView">
<forms:SKCanvasView x:Name="SkCanvasView"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand"/>
</ContentView>

View File

@ -1,110 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SkiaSharp;
using SkiaSharp.Views.Forms;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public partial class CircularProgressbarView : ContentView
{
private ProgressDrawer _progressDrawer;
public static readonly BindableProperty ProgressProperty = BindableProperty.Create(
"Progress", typeof(double), typeof(CircularProgressbarView), propertyChanged: OnProgressChanged);
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
public float Radius { get; set; }
public float StrokeWidth { get; set; }
public Color ProgressColor { get; set; }
public Color EndingProgressColor { get; set; }
public Color BackgroundProgressColor { get; set; }
private static void OnProgressChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var context = bindable as CircularProgressbarView;
context.SkCanvasView.InvalidateSurface();
}
public CircularProgressbarView()
{
InitializeComponent();
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(Progress) && _progressDrawer == null)
{
var circle = new Circle(Radius * (float)DeviceDisplay.MainDisplayInfo.Density, (info) => new SKPoint((float)info.Width / 2, (float)info.Height / 2));
_progressDrawer = new ProgressDrawer(SkCanvasView, circle, () => (float)Progress, StrokeWidth * (float)DeviceDisplay.MainDisplayInfo.Density, BackgroundProgressColor.ToSKColor(), ProgressColor.ToSKColor(), EndingProgressColor.ToSKColor());
}
}
}
public class Circle
{
private readonly Func<SKImageInfo, SKPoint> _centerfunc;
public Circle(float redius, Func<SKImageInfo, SKPoint> centerfunc)
{
_centerfunc = centerfunc;
Redius = redius;
}
public SKPoint Center { get; set; }
public float Redius { get; set; }
public SKRect Rect => new SKRect(Center.X - Redius, Center.Y - Redius, Center.X + Redius, Center.Y + Redius);
public void CalculateCenter(SKImageInfo argsInfo)
{
Center = _centerfunc.Invoke(argsInfo);
}
}
public class ProgressDrawer
{
public ProgressDrawer(SKCanvasView canvas, Circle circle, Func<float> progress, float strokeWidth, SKColor progressColor, SKColor foregroundColor, SKColor progressEndColor)
{
canvas.PaintSurface += (sender, args) =>
{
circle.CalculateCenter(args.Info);
args.Surface.Canvas.Clear();
DrawCircle(args.Surface.Canvas, circle, strokeWidth, progressColor);
DrawArc(args.Surface.Canvas, circle, progress, strokeWidth, foregroundColor, progressEndColor);
};
}
private void DrawCircle(SKCanvas canvas, Circle circle, float strokewidth, SKColor color)
{
canvas.DrawCircle(circle.Center, circle.Redius,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = color,
IsStroke = true,
IsAntialias = true
});
}
private void DrawArc(SKCanvas canvas, Circle circle, Func<float> progress, float strokewidth, SKColor color, SKColor progressEndColor)
{
var progressValue = progress.Invoke();
var angle = progressValue * 3.6f;
canvas.DrawArc(circle.Rect, 270, angle, false,
new SKPaint()
{
StrokeWidth = strokewidth,
Color = progressValue < 20f ? progressEndColor : color,
IsStroke = true,
IsAntialias = true
});
}
}
}

View File

@ -11,6 +11,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Forms;
namespace Bit.App.Pages
@ -92,7 +93,7 @@ namespace Bit.App.Pages
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
CopyCommand = new Command(CopyTotpClipboard);
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync);
Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
@ -154,7 +155,7 @@ namespace Bit.App.Pages
public Command UriOptionsCommand { get; set; }
public Command FieldOptionsCommand { get; set; }
public Command PasswordPromptHelpCommand { get; set; }
public Command CopyCommand { get; set; }
public AsyncCommand CopyCommand { get; set; }
public string CipherId { get; set; }
public string OrganizationId { get; set; }
public string FolderId { get; set; }
@ -305,8 +306,8 @@ namespace Bit.App.Pages
public bool AllowPersonal { get; set; }
public bool PasswordPrompt => Cipher.Reprompt != CipherRepromptType.None;
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
public bool HasTotpValue => !string.IsNullOrEmpty(Cipher.Login.Totp);
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTOTP}";
public bool HasTotpValue => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
public void Init()
{
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
@ -865,10 +866,17 @@ namespace Bit.App.Pages
}
}
private async void CopyTotpClipboard()
private async Task CopyTotpClipboardAsync()
{
await _clipboardService.CopyTextAsync(_cipher.Login.Totp);
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
try
{
await _clipboardService.CopyTextAsync(_cipher.Login.Totp);
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
}

View File

@ -6,6 +6,7 @@
xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:effects="clr-namespace:Bit.App.Effects"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
x:DataType="pages:GroupingsPageViewModel"
Title="{Binding PageTitle}"
@ -143,7 +144,6 @@
<StackLayout
IsVisible="{Binding ShowTotpFilter}"
Orientation="Horizontal"
HorizontalOptions="FillAndExpand"
Margin="0,5,10,0">
<Label
Text="{u:I18n DisplayItemsContainingTOTP}"
@ -158,7 +158,14 @@
IsToggled="{Binding TotpFilterEnable}"
StyleClass="box-value"
HorizontalOptions="End"
Toggled="TotpFilter_Toggled" />
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{Binding ShowTotpCodesAccessibilityText}">
<Switch.Behaviors>
<xct:EventToCommandBehavior
EventName="Toggled"
Command="{Binding TotpFilterCommand}" />
</Switch.Behaviors>
</Switch>
</StackLayout>
<StackLayout

View File

@ -189,11 +189,11 @@ namespace Bit.App.Pages
return false;
}
protected override void OnDisappearing()
protected override async void OnDisappearing()
{
base.OnDisappearing();
IsBusy = false;
_vm.TotpFilterEnable = false;
await _vm.StopCiphersTotpTick();
_broadcasterService.Unsubscribe(_pageName);
_vm.DisableRefreshing();
_accountAvatar?.OnDisappearing();
@ -305,10 +305,5 @@ namespace Bit.App.Pages
{
await _accountListOverlay.HideAsync();
}
void TotpFilter_Toggled(System.Object sender, Xamarin.Forms.ToggledEventArgs e)
{
_vm.TotpFilterCommand.Execute(null);
}
}
}

View File

@ -12,6 +12,7 @@ namespace Bit.App.Pages
{
public class GroupingsPageTOTPListItem : ExtendedViewModel, IGroupingsPageListItem
{
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private readonly ITotpService _totpService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService;
@ -23,8 +24,8 @@ namespace Bit.App.Pages
public int interval { get; set; }
private double _progress;
private string _totpSec;
private string _totpCode;
private string _totpCodeFormatted = "938 928";
private string _totpCodeFormatted;
private TotpHelper _totpTickHelper;
public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled)
@ -39,10 +40,9 @@ namespace Bit.App.Pages
CopyCommand = new AsyncCommand(CopyToClipboardAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
_totpTickHelper = new TotpHelper(cipherView);
}
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public AsyncCommand CopyCommand { get; set; }
public CipherView Cipher
@ -110,39 +110,10 @@ namespace Bit.App.Pages
public async Task TotpTickAsync()
{
var epoc = CoreHelpers.EpocUtcNow() / 1000;
var mod = epoc % interval;
var totpSec = interval - mod;
TotpSec = totpSec.ToString();
Progress = totpSec * 100 / 30;
//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;
}
await _totpTickHelper.GenerateNewTotpValues();
TotpSec = _totpTickHelper.TotpSec;
Progress = _totpTickHelper.Progress;
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
@ -38,7 +39,8 @@ namespace Bit.App.Pages
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
private int _deletedCount = 0;
private CancellationTokenSource _totpTickCancellationToken;
private Task _totpTickTask;
private readonly ICipherService _cipherService;
private readonly IFolderService _folderService;
private readonly ICollectionService _collectionService;
@ -99,6 +101,9 @@ namespace Bit.App.Pages
public bool HasCiphers { get; set; }
public bool HasFolders { get; set; }
public bool HasCollections { get; set; }
public string ShowTotpCodesAccessibilityText => TotpFilterEnable ?
AppResources.AuthenticationCodesListIsVisibleActivateToShowCipherList
: AppResources.CipherListIsVisibleActivateToShowAuthenticationCodesList;
public bool ShowNoFolderCipherGroup => NoFolderCiphers != null
&& NoFolderCiphers.Count < NoFolderListSize
&& (Collections is null || !Collections.Any());
@ -292,33 +297,7 @@ namespace Bit.App.Pages
}
if (Ciphers?.Any() ?? false)
{
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)
{
await item.TotpUpdateCodeAsync();
}
Device.StartTimer(new TimeSpan(0, 0, 1), () =>
{
foreach (var item in ciphersListItems)
{
item.TotpTickAsync();
}
return TotpFilterEnable;
});
}
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()));
}
CreateCipherGroupedItems(ref groupedItems);
}
if (ShowNoFolderCipherGroup)
{
@ -406,6 +385,45 @@ namespace Bit.App.Pages
}
}
private void CreateCipherGroupedItems(ref List<GroupingsPageListGroup> groupedItems)
{
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
_totpTickCancellationToken?.Cancel();
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()));
StartCiphersTotpTick(ciphersListItems);
}
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()));
}
}
private void StartCiphersTotpTick(List<GroupingsPageTOTPListItem> ciphersListItems)
{
_totpTickCancellationToken?.Cancel();
_totpTickCancellationToken = new CancellationTokenSource();
_totpTickTask = new TimerTask(() => { ciphersListItems.ForEach(i => i.TotpTickAsync()); }, _totpTickCancellationToken).Run();
}
public async Task StopCiphersTotpTick()
{
TotpFilterEnable = false;
_totpTickCancellationToken?.Cancel();
if (_totpTickTask != null)
{
await _totpTickTask;
}
}
public void DisableRefreshing()
{
Refreshing = false;

View File

@ -70,7 +70,7 @@
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
HorizontalOptions="Center"
VerticalOptions="Start"
FontSize="30"
FontSize="Title"
TextColor="Transparent"/>
</StackLayout>
<BoxView
@ -88,8 +88,8 @@
Grid.RowSpan="2"
Margin="30,0">
<Label
Text="{u:I18n EnterCodeManually}"
FontSize="30" />
Text="{u:I18n EnterKeyManually}"
FontSize="Title" />
<Label
Text="{u:I18n AuthenticatorKeyScanner}"
StyleClass="box-label" />
@ -121,12 +121,7 @@
AutomationId="zxingDefaultOverlay_TopTextLabel"
Margin="30,15,30,0"
HorizontalOptions="Center"
StyleClass="text-sm"
TextColor="White" />
<Label
Text="{Binding CameraInstructionBottom}"
AutomationId="zxingDefaultOverlay_BottomTextLabel"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
StyleClass="text-sm"
TextColor="White" />
</StackLayout>

View File

@ -112,33 +112,44 @@ namespace Bit.App.Pages
private async void OnScanResult(ZXing.Result result)
{
// Stop analysis until we navigate away so we don't keep reading barcodes
_zxing.IsAnalyzing = false;
var text = result?.Text;
if (!string.IsNullOrWhiteSpace(text))
try
{
if (text.StartsWith("otpauth://totp"))
// Stop analysis until we navigate away so we don't keep reading barcodes
_zxing.IsAnalyzing = false;
var text = result?.Text;
if (!string.IsNullOrWhiteSpace(text))
{
await QrCodeFoundAsync();
_callback(text);
return;
}
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
!string.IsNullOrWhiteSpace(uri?.Query))
{
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
foreach (var part in queryParts)
if (text.StartsWith("otpauth://totp"))
{
if (part.StartsWith("secret="))
await QrCodeFoundAsync();
_callback(text);
return;
}
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
!string.IsNullOrWhiteSpace(uri?.Query))
{
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
foreach (var part in queryParts)
{
await QrCodeFoundAsync();
_callback(part.Substring(7)?.ToUpperInvariant());
return;
if (part.StartsWith("secret="))
{
await QrCodeFoundAsync();
var subResult = part.Substring(7);
if (!string.IsNullOrEmpty(subResult))
{
_callback(subResult.ToUpperInvariant());
}
return;
}
}
}
}
_callback(null);
}
catch (Exception ex)
{
_logger?.Value?.Exception(ex);
}
_callback(null);
}
private async Task QrCodeFoundAsync()
@ -228,23 +239,35 @@ namespace Bit.App.Pages
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize, startYPoint + squareSize - lineSize, strokePaint);
}
}
async Task AnimationLoopAsync()
{
_stopwatch.Start();
while (_pageIsActive)
try
{
var t = _stopwatch.Elapsed.TotalSeconds % 2 / 2;
_scale = (20 - (1 - (float)Math.Sin(4 * Math.PI * t))) / 20;
SkCanvasView.InvalidateSurface();
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
if (_qrcodeFound && _scale > 0.98f)
_stopwatch.Start();
while (_pageIsActive)
{
_checkIcon.TextColor = _greenColor;
var t = _stopwatch.Elapsed.TotalSeconds % 2 / 2;
_scale = (20 - (1 - (float)Math.Sin(4 * Math.PI * t))) / 20;
SkCanvasView.InvalidateSurface();
break;
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
if (_qrcodeFound && _scale > 0.98f)
{
_checkIcon.TextColor = _greenColor;
SkCanvasView.InvalidateSurface();
break;
}
}
_stopwatch.Stop();
}
catch (Exception ex)
{
_logger?.Value?.Exception(ex);
}
finally
{
_stopwatch?.Stop();
}
_stopwatch.Stop();
}
}
}

View File

@ -7,19 +7,17 @@ namespace Bit.App.Pages
{
public class ScanPageViewModel : BaseViewModel
{
private bool _showScanner;
private bool _showScanner = true;
private string _totpAuthenticationKey;
public ScanPageViewModel()
{
ShowScanner = true;
ToggleScanModeCommand = new Command(ToggleScanAsync);
ToggleScanModeCommand = new Command(() => ShowScanner = !ShowScanner);
}
public Command ToggleScanModeCommand { get; set; }
public string ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner;
public string CameraInstructionTop => ShowScanner ? AppResources.CameraInstructionTop : AppResources.OnceTheKeyIsSuccessfullyEntered;
public string CameraInstructionBottom => ShowScanner ? AppResources.CameraInstructionBottom : AppResources.SelectAddTotpToStoreTheKeySafely;
public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered;
public string TotpAuthenticationKey
{
get => _totpAuthenticationKey;
@ -37,8 +35,7 @@ namespace Bit.App.Pages
{
nameof(ToggleScanModeLabel),
nameof(ScanQrPageTitle),
nameof(CameraInstructionTop),
nameof(CameraInstructionBottom)
nameof(CameraInstructionTop)
});
}
@ -54,16 +51,11 @@ namespace Bit.App.Pages
});
fs.Spans.Add(new Span
{
Text = ShowScanner ? AppResources.EnterCodeManually : AppResources.ScanQRCode,
Text = ShowScanner ? AppResources.EnterKeyManually : AppResources.ScanQRCode,
TextColor = ThemeManager.GetResourceColor("ScanningToggleModeTextColor")
});
return fs;
}
}
private void ToggleScanAsync()
{
ShowScanner = !ShowScanner;
}
}
}

View File

@ -187,11 +187,6 @@
VerticalOptions="Start" />
<controls:CircularProgressbarView
Progress="{Binding TotpProgress}"
ProgressColor="{StaticResource PrimaryColor}"
EndingProgressColor="{StaticResource DangerColor}"
BackgroundProgressColor="{StaticResource BackgroundColor}"
StrokeWidth="3"
Radius="15"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
@ -203,6 +198,7 @@
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="2"
StyleClass="text-sm"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Center"
HorizontalOptions="FillAndExpand"
@ -219,7 +215,7 @@
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n CopyTotp}" />
<Label
Text="{Binding UpgradeToPremiumTotpText}"
Text="{u:I18n UpgradeToPremiumTotpText}"
StyleClass="box-footer-label"
IsVisible="{Binding ShowUpgradePremiumTotpText}"
Margin="0,5,0,2"

View File

@ -107,12 +107,12 @@ namespace Bit.App.Pages
}, _mainContent);
}
protected override void OnDisappearing()
protected override async void OnDisappearing()
{
base.OnDisappearing();
IsBusy = false;
_broadcasterService.Unsubscribe(nameof(ViewPage));
_vm.CleanUp();
await _vm.StopCiphersTotpTick();
}
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions;
@ -22,7 +23,6 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService;
private readonly IStateService _stateService;
private readonly ITotpService _totpService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuditService _auditService;
private readonly IMessagingService _messagingService;
@ -47,13 +47,15 @@ namespace Bit.App.Pages
private byte[] _attachmentData;
private string _attachmentFilename;
private bool _passwordReprompted;
private TotpHelper _totpTickHelper;
private CancellationTokenSource _totpTickCancellationToken;
private Task _totpTickTask;
public ViewPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
@ -208,7 +210,6 @@ namespace Bit.App.Pages
}
}
public string UpgradeToPremiumTotpText => AppResources.PremiumSubscriptionRequired;
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && ShowTotp;
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
public bool ShowIdentityAddress => IsIdentity && (
@ -254,7 +255,6 @@ namespace Bit.App.Pages
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
{
CleanUp();
var cipher = await _cipherService.GetAsync(CipherId);
if (cipher == null)
{
@ -268,19 +268,10 @@ namespace Bit.App.Pages
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;
});
_totpTickHelper = new TotpHelper(Cipher);
_totpTickCancellationToken?.Cancel();
_totpTickCancellationToken = new CancellationTokenSource();
_totpTickTask = new TimerTask(StartCiphersTotpTick, _totpTickCancellationToken).Run();
}
if (_previousCipherId != CipherId)
{
@ -291,9 +282,20 @@ namespace Bit.App.Pages
return true;
}
public void CleanUp()
private async void StartCiphersTotpTick()
{
_totpInterval = null;
await _totpTickHelper.GenerateNewTotpValues();
TotpSec = _totpTickHelper.TotpSec;
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
}
public async Task StopCiphersTotpTick()
{
_totpTickCancellationToken?.Cancel();
if (_totpTickTask != null)
{
await _totpTickTask;
}
}
public async void TogglePassword()
@ -421,47 +423,6 @@ namespace Bit.App.Pages
return false;
}
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 CheckPasswordAsync()
{
if (!(Page as BaseContentPage).DoOnce())
@ -647,7 +608,7 @@ namespace Bit.App.Pages
}
else if (id == "LoginTotp")
{
text = _totpCode;
text = TotpCodeFormatted.Replace(" ", string.Empty);
name = AppResources.VerificationCodeTotp;
}
else if (id == "LoginUri")

View File

@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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
// the code is regenerated.
@ -9,9 +10,10 @@
namespace Bit.App.Resources {
using System;
using System.Reflection;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.1.0.0")]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class AppResources {
@ -1449,15 +1451,9 @@ namespace Bit.App.Resources {
}
}
public static string CameraInstructionBottom {
public static string PointYourCameraAtTheQRCode {
get {
return ResourceManager.GetString("CameraInstructionBottom", resourceCulture);
}
}
public static string CameraInstructionTop {
get {
return ResourceManager.GetString("CameraInstructionTop", resourceCulture);
return ResourceManager.GetString("PointYourCameraAtTheQRCode", resourceCulture);
}
}
@ -4077,9 +4073,9 @@ namespace Bit.App.Resources {
}
}
public static string EnterCodeManually {
public static string EnterKeyManually {
get {
return ResourceManager.GetString("EnterCodeManually", resourceCulture);
return ResourceManager.GetString("EnterKeyManually", resourceCulture);
}
}
@ -4089,9 +4085,9 @@ namespace Bit.App.Resources {
}
}
public static string SetupTOTP {
public static string SetupTotp {
get {
return ResourceManager.GetString("SetupTOTP", resourceCulture);
return ResourceManager.GetString("SetupTotp", resourceCulture);
}
}
@ -4106,13 +4102,23 @@ namespace Bit.App.Resources {
return ResourceManager.GetString("SelectAddTotpToStoreTheKeySafely", resourceCulture);
}
}
public static string NeverLockWarning
{
get
{
public static string NeverLockWarning {
get {
return ResourceManager.GetString("NeverLockWarning", resourceCulture);
}
}
public static string CipherListIsVisibleActivateToShowAuthenticationCodesList {
get {
return ResourceManager.GetString("CipherListIsVisibleActivateToShowAuthenticationCodesList", resourceCulture);
}
}
public static string AuthenticationCodesListIsVisibleActivateToShowCipherList {
get {
return ResourceManager.GetString("AuthenticationCodesListIsVisibleActivateToShowCipherList", resourceCulture);
}
}
}
}

View File

@ -893,11 +893,9 @@
<data name="AuthenticatorKeyReadError" xml:space="preserve">
<value>Cannot read authenticator key.</value>
</data>
<data name="CameraInstructionBottom" xml:space="preserve">
<value>Scanning will happen automatically.</value>
</data>
<data name="CameraInstructionTop" xml:space="preserve">
<value>Point your camera at the QR code.</value>
<data name="PointYourCameraAtTheQRCode" xml:space="preserve">
<value>Point your camera at the QR Code.
Scanning will happen automatically.</value>
</data>
<data name="ScanQrTitle" xml:space="preserve">
<value>Scan QR Code</value>
@ -2276,22 +2274,29 @@
<data name="AuthenticatorKeyScanner" xml:space="preserve">
<value>Authenticator Key</value>
</data>
<data name="EnterCodeManually" xml:space="preserve">
<data name="EnterKeyManually" xml:space="preserve">
<value>Enter Key Manually</value>
</data>
<data name="AddTotp" xml:space="preserve">
<value>Add TOTP</value>
</data>
<data name="SetupTOTP" xml:space="preserve">
<data name="SetupTotp" xml:space="preserve">
<value>Set up TOTP</value>
</data>
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
<value>Once the key is successfully entered,</value>
<value>Once the key is successfully entered,
select Add TOTP to store the key safely</value>
</data>
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
<value>select Add TOTP to store the key safely</value>
<value></value>
</data>
<data name="NeverLockWarning" xml:space="preserve">
<value>Setting your lock options to “Never” keeps your vault available to anyone with access to your device. If you use this option, you should ensure that you keep your device properly protected.</value>
</data>
<data name="CipherListIsVisibleActivateToShowAuthenticationCodesList" xml:space="preserve">
<value>Cipher list is visible, activate to show authentication codes list.</value>
</data>
<data name="AuthenticationCodesListIsVisibleActivateToShowCipherList" xml:space="preserve">
<value>Authentication codes list is visible, activate to show cipher list.</value>
</data>
</root>

View File

@ -505,4 +505,17 @@
</Keyboard>
</Setter>
</Style>
<Style TargetType="controls:CircularProgressbarView">
<Setter Property="ProgressColor"
Value="{DynamicResource PrimaryColor}" />
<Setter Property="EndingProgressColor"
Value="{DynamicResource DangerColor}" />
<Setter Property="BackgroundProgressColor"
Value="{DynamicResource BackgroundColor}" />
<Setter Property="StrokeWidth"
Value="3" />
<Setter Property="Radius"
Value="15" />
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,56 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
namespace Bit.App.Utilities
{
public class TimerTask
{
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private readonly Action _action;
private readonly CancellationTokenSource _cancellationToken;
public TimerTask(Action action, CancellationTokenSource cancellationToken)
{
_action = action;
_cancellationToken = cancellationToken;
}
public Task Run()
{
return Task.Run(async () =>
{
try
{
while (!_cancellationToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), _cancellationToken.Token);
await Device.InvokeOnMainThreadAsync(() =>
{
if (!_cancellationToken.IsCancellationRequested)
{
try
{
_action?.Invoke();
}
catch (Exception ex)
{
_logger?.Value?.Exception(ex);
}
}
});
Console.WriteLine("TESTES TASK RUNNING");
}
}
catch (TaskCanceledException) { }
catch (Exception ex)
{
_logger?.Value?.Exception(ex);
}
}, _cancellationToken.Token);
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
namespace Bit.App.Utilities
{
public class TotpHelper
{
private ITotpService _totpService;
private int _interval;
public TotpHelper(CipherView cipher)
{
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
Cipher = cipher;
_interval = _totpService.GetTimeInterval(cipher?.Login?.Totp);
}
public CipherView Cipher { get; private set; }
public string TotpSec { get; private set; }
public string TotpCodeFormatted { get; private set; }
public double Progress { get; private set; }
public async Task GenerateNewTotpValues()
{
var epoc = CoreHelpers.EpocUtcNow() / 1000;
var mod = epoc % _interval;
var totpSec = _interval - mod;
TotpSec = totpSec.ToString();
Progress = totpSec * 100 / 30;
if (mod == 0 || string.IsNullOrEmpty(TotpCodeFormatted))
{
TotpCodeFormatted = await TotpUpdateCodeAsync();
}
}
private async Task<string> TotpUpdateCodeAsync()
{
var totpCode = await _totpService.GetCodeAsync(Cipher?.Login?.Totp);
if (totpCode != null)
{
if (totpCode.Length > 4)
{
var half = (int)Math.Floor(totpCode.Length / 2M);
return string.Format("{0} {1}", totpCode.Substring(0, half),
totpCode.Substring(half));
}
else
{
return totpCode;
}
}
else
{
return null;
}
}
}
}