mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-22 11:35:21 +01:00
[SG-416] Updates to Bitwarden Authenticator (Feature Branch) (#2041)
* Initial commit of new TOTP page * Revert config files from previous commit This reverts commitb02c58e362
. * clear extra code and fix build * add tab page * add authentication view cell * add toolbar icons * got the countdown working * enable context loading and vm init * PS-70 Added toggle to quickly filter TOTP cypher items and show their details, Added new text resource * PS-70 removed old authentication tab * removed unnecessary code on vm * fixed formatting * PS-70 Added circular progress to the OTP count down * PS-70 Fixed grid cell width. Added red progress at 20 percent. Refactored circular progress view. * PS-70 Added new props to custom control. * PS-70 show toggle only if it's premium * PS-70 removed unnecessary code * PS-70 add copy to clipboard. * PS-70 show upgrade to premium text on details to free user. * PS-70 added text labels to resource files * PS-70 Renamed TOTP to Totp to have consistency in naming. Removed a11y text of switch because android was overlapping text. * PS-70 added new UI to enter code manually in the QR Code scanner screen. Changed existing labels on scanner screen. * PS-70 Splited totp code to adjust spacing. * PS-70 Added scanner square corner overlay. Added scanning animation. Added scan success animation. * PS-70 let zxing scanner camera feed on until screen is closed. * PS-70 fixed scanner animation for android devices * PS-70 added vibrate permission to manifest. refactored scanpage code. added manual authentication key feature in scanner. * PS-70 fixed totp cell title label font * PS-70 added copy button to totp edit cipher. Added row button when totp is null. * PS-70 changed labels on manual scanner screen * PS-70 Added label on top of button to solve UI bug. * PS-70 Fixed android button overlapping bug by adding button styling to a Frame view and placing a label inside. Fixed Color on scanner page. * PS-70 Added frame styling for iOS, since frame view has different base configuration for android and iOS. * PS-70 fixed font clipping bug on iOS * PS-70 removed shadow for newer versions of android * PS-70 code format * PS-70 removed update to premium uri launch * PS-70 PR fix for AppResource vs code behind generation. * PS-70 changed premium required label. fixed bug when to show premium required label. * [SSG-416] Removed the dashes from free user and just left the Premium subscription required. * [SSG-416] removed unnecessary changes to the TabsPage file * [SSG-416] removed unnecessary using. * [SSG-416] Updated ViewPageViewModel and code refactoring. * [SG-416] Updated scanner mode toggle text color to be inline with figma designs * [SSG-416] Mobile PR Fixes * [SSG-416] Add to remove a11y text from totp toggle because on android it places an helper text next to the switch making it invisible. Also removed from the label because it already reads the text from the label * [SSG-416] run dotnet tool run dotnet-format * [SSG-416] PR fixes * Revert "[SSG-416] PR fixes" This reverts commit2f2b90acee
. * [PS-416] Fixed a bug where the item details page was not updating after saving. * [SG-416] Authenticator toggle remake (#2027) * [SG-416] Removed toggle to TOTP. Added on MainPage new entry to go to screen with TOTP codes. Added filter for TOTP codes to be used when searching. * [SG-416] Removed unnecessary code. Added nav back if there is only 1 cipher with totp code and the user removes it. * [SG-416] Run dotnet format tool * [SG-416] PR fixes * [SG-416] PR Fixes. Manifest formatting. Add try catch. Extracted method and added null protection. * [SG-416] Make TOTP codes appear above favourites. * [SG-416] PR fixes. Show error dialog. Co-authored-by: Carlos J. Muentes <42616259+cmuentes@users.noreply.github.com> Co-authored-by: Jacob Fink <jfink@bitwarden.com>
This commit is contained in:
parent
3d9555d420
commit
e829279e29
@ -9,6 +9,7 @@
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
|
@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<controls:ExtendedGrid xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Controls.AuthenticatorViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:ff="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
StyleClass="list-row, list-row-platform"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
x:DataType="pages:GroupingsPageTOTPListItem"
|
||||
ColumnDefinitions="40,*,40,Auto,40"
|
||||
RowSpacing="0"
|
||||
Padding="0,10,0,0"
|
||||
RowDefinitions="*,*">
|
||||
|
||||
<Grid.Resources>
|
||||
<u:IconGlyphConverter x:Key="iconGlyphConverter" />
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</Grid.Resources>
|
||||
|
||||
<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" />
|
||||
|
||||
<ff:CachedImage
|
||||
Grid.Column="0"
|
||||
BitmapOptimizations="True"
|
||||
ErrorPlaceholder="login.png"
|
||||
LoadingPlaceholder="login.png"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
WidthRequest="22"
|
||||
HeightRequest="22"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding ShowIconImage}"
|
||||
Source="{Binding IconImageSource, Mode=OneTime}"
|
||||
AutomationProperties.IsInAccessibleTree="False" />
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
VerticalTextAlignment="Center"
|
||||
VerticalOptions="Fill"
|
||||
StyleClass="list-title, list-title-platform"
|
||||
Text="{Binding Cipher.Name}" />
|
||||
|
||||
<Label
|
||||
LineBreakMode="TailTruncation"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
VerticalTextAlignment="Center"
|
||||
VerticalOptions="Fill"
|
||||
StyleClass="list-subtitle, list-subtitle-platform"
|
||||
Text="{Binding Cipher.SubTitle}" />
|
||||
|
||||
<controls:CircularProgressbarView
|
||||
Progress="{Binding Progress}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
HorizontalOptions="Fill"
|
||||
VerticalOptions="CenterAndExpand" />
|
||||
|
||||
<Label
|
||||
Text="{Binding TotpSec, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
StyleClass="text-sm"
|
||||
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="Fill"
|
||||
VerticalOptions="Fill">
|
||||
|
||||
<controls:MonoLabel
|
||||
Text="{Binding TotpCodeFormattedStart, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalTextAlignment="Center"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
|
||||
<controls:MonoLabel
|
||||
Text="{Binding TotpCodeFormattedEnd, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
StyleClass="text-lg"
|
||||
HorizontalTextAlignment="Center"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
</StackLayout>
|
||||
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="LoginTotp"
|
||||
Grid.Row="0"
|
||||
Grid.Column="4"
|
||||
Grid.RowSpan="2"
|
||||
Padding="0,0,1,0"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||
</controls:ExtendedGrid>
|
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using Bit.App.Pages;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public partial class AuthenticatorViewCell : ExtendedGrid
|
||||
{
|
||||
public static readonly BindableProperty CipherProperty = BindableProperty.Create(
|
||||
nameof(Cipher), typeof(CipherView), typeof(AuthenticatorViewCell), default(CipherView), BindingMode.TwoWay);
|
||||
|
||||
public static readonly BindableProperty WebsiteIconsEnabledProperty = BindableProperty.Create(
|
||||
nameof(WebsiteIconsEnabled), typeof(bool?), typeof(AuthenticatorViewCell));
|
||||
|
||||
public static readonly BindableProperty TotpSecProperty = BindableProperty.Create(
|
||||
nameof(TotpSec), typeof(long), typeof(AuthenticatorViewCell));
|
||||
|
||||
public AuthenticatorViewCell()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public Command CopyCommand { get; set; }
|
||||
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => GetValue(CipherProperty) as CipherView;
|
||||
set => SetValue(CipherProperty, value);
|
||||
}
|
||||
|
||||
public bool? WebsiteIconsEnabled
|
||||
{
|
||||
get => (bool)GetValue(WebsiteIconsEnabledProperty);
|
||||
set => SetValue(WebsiteIconsEnabledProperty, value);
|
||||
}
|
||||
|
||||
public long TotpSec
|
||||
{
|
||||
get => (long)GetValue(TotpSecProperty);
|
||||
set => SetValue(TotpSecProperty, value);
|
||||
}
|
||||
|
||||
public bool ShowIconImage
|
||||
{
|
||||
get => WebsiteIconsEnabled ?? false
|
||||
&& !string.IsNullOrWhiteSpace(Cipher.Login?.Uri)
|
||||
&& IconImageSource != null;
|
||||
}
|
||||
|
||||
private string _iconImageSource = string.Empty;
|
||||
public string IconImageSource
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_iconImageSource == string.Empty) // default value since icon source can return null
|
||||
{
|
||||
_iconImageSource = IconImageHelper.GetLoginIconImage(Cipher);
|
||||
}
|
||||
return _iconImageSource;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
139
src/App/Controls/CircularProgressbarView.cs
Normal file
139
src/App/Controls/CircularProgressbarView.cs
Normal 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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -184,31 +184,61 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n AuthenticatorKey}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<Frame
|
||||
IsVisible="{Binding HasTotpValue, Converter={StaticResource inverseBool}}"
|
||||
Margin="0,5,0,0"
|
||||
StyleClass="btn-icon-row"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
Padding="0"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ScanTotp_Clicked" />
|
||||
</Frame.GestureRecognizers>
|
||||
<controls:IconLabel
|
||||
Text="{Binding SetupTotpText}"
|
||||
Padding="0,15"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="FillAndExpand"
|
||||
VerticalTextAlignment="Center" />
|
||||
</Frame>
|
||||
<controls:MonoEntry
|
||||
x:Name="_loginTotpEntry"
|
||||
Text="{Binding Cipher.Login.Totp}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
IsPassword="{Binding Cipher.ViewPassword, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding Cipher.ViewPassword}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Camera}}"
|
||||
Clicked="ScanTotp_Clicked"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
IsVisible="{Binding HasTotpValue}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
||||
</Grid>
|
||||
|
@ -10,6 +10,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
|
||||
@ -24,6 +25,7 @@ namespace Bit.App.Pages
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
|
||||
private bool _showNotesSeparator;
|
||||
private bool _showPassword;
|
||||
@ -47,6 +49,7 @@ namespace Bit.App.Pages
|
||||
nameof(ShowUris),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowCollections),
|
||||
nameof(HasTotpValue)
|
||||
};
|
||||
|
||||
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
|
||||
@ -71,6 +74,7 @@ namespace Bit.App.Pages
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
|
||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@ -79,6 +83,7 @@ namespace Bit.App.Pages
|
||||
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
|
||||
FieldOptionsCommand = new Command<CipherAddEditPageFieldViewModel>(FieldOptions);
|
||||
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
|
||||
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
||||
Uris = new ExtendedObservableCollection<LoginUriView>();
|
||||
Fields = new ExtendedObservableCollection<CipherAddEditPageFieldViewModel>();
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
@ -139,6 +144,7 @@ namespace Bit.App.Pages
|
||||
public Command UriOptionsCommand { get; set; }
|
||||
public Command FieldOptionsCommand { get; set; }
|
||||
public Command PasswordPromptHelpCommand { get; set; }
|
||||
public AsyncCommand CopyCommand { get; set; }
|
||||
public string CipherId { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public string FolderId { get; set; }
|
||||
@ -284,7 +290,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 => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
|
||||
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
|
||||
public void Init()
|
||||
{
|
||||
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
|
||||
@ -818,6 +825,19 @@ namespace Bit.App.Pages
|
||||
{
|
||||
TriggerPropertyChanged(nameof(Cipher), AdditionalPropertiesToRaiseOnCipherChanged);
|
||||
}
|
||||
|
||||
private async Task CopyTotpClipboardAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(Cipher.Login.Totp);
|
||||
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.AuthenticatorKeyScanner));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CipherAddEditPageFieldViewModel : ExtendedViewModel
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.CipherDetailsPage"
|
||||
@ -22,15 +22,15 @@
|
||||
<u:StringHasValueConverter x:Key="stringHasValue" />
|
||||
<u:IsNotNullConverter x:Key="notNull" />
|
||||
<ToolbarItem Text="{u:I18n Collections}"
|
||||
x:Key="collectionsItem"
|
||||
x:Name="_collectionsItem"
|
||||
Clicked="Collections_Clicked"
|
||||
Order="Secondary" />
|
||||
x:Key="collectionsItem"
|
||||
x:Name="_collectionsItem"
|
||||
Clicked="Collections_Clicked"
|
||||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n MoveToOrganization}"
|
||||
x:Key="shareItem"
|
||||
x:Name="_shareItem"
|
||||
Clicked="Share_Clicked"
|
||||
Order="Secondary" />
|
||||
x:Key="shareItem"
|
||||
x:Name="_shareItem"
|
||||
Clicked="Share_Clicked"
|
||||
Order="Secondary" />
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||
x:Name="_closeItem" x:Key="closeItem" />
|
||||
<ToolbarItem Clicked="EditToolbarItem_Clicked" Order="Primary"
|
||||
@ -83,7 +83,7 @@
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@ -96,7 +96,7 @@
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Login.Username, Converter={StaticResource stringHasValue}}" />
|
||||
<Grid StyleClass="box-row"
|
||||
<Grid StyleClass="box-row"
|
||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@ -126,7 +126,7 @@
|
||||
Grid.Column="0"
|
||||
LineBreakMode="CharacterWrap"
|
||||
IsVisible="{Binding ShowPassword}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||
Command="{Binding CheckPasswordCommand}"
|
||||
@ -136,7 +136,7 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
@ -147,7 +147,7 @@
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
AutomationProperties.HelpText="{Binding PasswordVisibilityAccessibilityText}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@ -165,10 +165,11 @@
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="40" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
@ -178,29 +179,49 @@
|
||||
Grid.Column="0" />
|
||||
<controls:MonoLabel
|
||||
Text="{Binding TotpCodeFormatted, Mode=OneWay}"
|
||||
IsVisible="{Binding ShowUpgradePremiumTotpText, Converter={StaticResource inverseBool}}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<Label
|
||||
Text="{Binding TotpSec, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
Margin="0, 0, 10, 0"
|
||||
Grid.Column="0"
|
||||
VerticalTextAlignment="Start"
|
||||
VerticalOptions="Start" />
|
||||
<controls:CircularProgressbarView
|
||||
Progress="{Binding TotpProgress}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
HorizontalOptions="End"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalOptions="CenterAndExpand" />
|
||||
<controls:IconButton
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
<Label
|
||||
Text="{Binding TotpSec, Mode=OneWay}"
|
||||
Style="{DynamicResource textTotp}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
StyleClass="text-sm"
|
||||
VerticalTextAlignment="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand" />
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
IsVisible="{Binding CanAccessPremium}"
|
||||
CommandParameter="LoginTotp"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyTotp}" />
|
||||
<Label
|
||||
Text="{u:I18n PremiumSubscriptionRequired}"
|
||||
StyleClass="box-footer-label"
|
||||
IsVisible="{Binding ShowUpgradePremiumTotpText}"
|
||||
Margin="0,5,0,2"
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator" IsVisible="{Binding ShowTotp}" />
|
||||
</StackLayout>
|
||||
@ -244,7 +265,7 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardNumber}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardNumberIcon}"
|
||||
Command="{Binding ToggleCardNumberCommand}"
|
||||
@ -253,7 +274,7 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@ -316,7 +337,7 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding ShowCardCode}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowCardCodeIcon}"
|
||||
Command="{Binding ToggleCardCodeCommand}"
|
||||
@ -325,7 +346,7 @@
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Clone}}"
|
||||
Command="{Binding CopyCommand}"
|
||||
@ -529,7 +550,7 @@
|
||||
<StackLayout StyleClass="box-row">
|
||||
<controls:SelectableLabel
|
||||
Text="{Binding Cipher.Notes, Mode=OneWay}"
|
||||
StyleClass="box-value"/>
|
||||
StyleClass="box-value" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
@ -590,7 +611,7 @@
|
||||
StyleClass="box-value"
|
||||
IsVisible="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}" />
|
||||
</StackLayout>
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowHiddenValueIcon}"
|
||||
Command="{Binding ToggleHiddenValueCommand}"
|
||||
@ -638,7 +659,7 @@
|
||||
StyleClass="box-sub-label"
|
||||
HorizontalTextAlignment="End"
|
||||
VerticalTextAlignment="Center" />
|
||||
<controls:IconButton
|
||||
<controls:IconButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.Download}}"
|
||||
Command="{Binding BindingContext.DownloadAttachmentCommand, Source={x:Reference _page}}"
|
||||
@ -702,4 +723,4 @@
|
||||
</Button>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
</pages:BaseContentPage>
|
@ -111,8 +111,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_vm.StopCiphersTotpTick().FireAndForget();
|
||||
_broadcasterService.Unsubscribe(nameof(CipherDetailsPage));
|
||||
_vm.CleanUp();
|
||||
}
|
||||
|
||||
private async void PasswordHistory_Tapped(object sender, System.EventArgs e)
|
||||
|
@ -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;
|
||||
@ -21,6 +22,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly ITotpService _totpService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IEventService _eventService;
|
||||
@ -42,11 +44,15 @@ namespace Bit.App.Pages
|
||||
private byte[] _attachmentData;
|
||||
private string _attachmentFilename;
|
||||
private bool _passwordReprompted;
|
||||
private TotpHelper _totpTickHelper;
|
||||
private CancellationTokenSource _totpTickCancellationToken;
|
||||
private Task _totpTickTask;
|
||||
|
||||
public CipherDetailsPageViewModel()
|
||||
{
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
||||
@ -91,6 +97,7 @@ namespace Bit.App.Pages
|
||||
nameof(ShowIdentityAddress),
|
||||
nameof(IsDeleted),
|
||||
nameof(CanEdit),
|
||||
nameof(ShowUpgradePremiumTotpText)
|
||||
};
|
||||
public List<CipherDetailsPageFieldViewModel> Fields
|
||||
{
|
||||
@ -191,21 +198,22 @@ namespace Bit.App.Pages
|
||||
return fs;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && ShowTotp;
|
||||
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
||||
public bool ShowIdentityAddress => IsIdentity && (
|
||||
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
|
||||
!string.IsNullOrWhiteSpace(Cipher.Identity.City) ||
|
||||
!string.IsNullOrWhiteSpace(Cipher.Identity.Country));
|
||||
public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null);
|
||||
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
||||
!string.IsNullOrWhiteSpace(TotpCodeFormatted);
|
||||
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp);
|
||||
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
||||
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
||||
public string TotpCodeFormatted
|
||||
{
|
||||
get => _totpCodeFormatted;
|
||||
get => _canAccessPremium ? _totpCodeFormatted : string.Empty;
|
||||
set => SetProperty(ref _totpCodeFormatted, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
@ -215,7 +223,11 @@ namespace Bit.App.Pages
|
||||
public string TotpSec
|
||||
{
|
||||
get => _totpSec;
|
||||
set => SetProperty(ref _totpSec, value);
|
||||
set => SetProperty(ref _totpSec, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(TotpProgress)
|
||||
});
|
||||
}
|
||||
public bool TotpLow
|
||||
{
|
||||
@ -226,12 +238,12 @@ namespace Bit.App.Pages
|
||||
Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"];
|
||||
}
|
||||
}
|
||||
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / 30;
|
||||
public bool IsDeleted => Cipher.IsDeleted;
|
||||
public bool CanEdit => !Cipher.IsDeleted;
|
||||
|
||||
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
||||
{
|
||||
CleanUp();
|
||||
var cipher = await _cipherService.GetAsync(CipherId);
|
||||
if (cipher == null)
|
||||
{
|
||||
@ -245,19 +257,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(_logger, StartCiphersTotpTick, _totpTickCancellationToken).RunPeriodic();
|
||||
}
|
||||
if (_previousCipherId != CipherId)
|
||||
{
|
||||
@ -268,9 +271,27 @@ namespace Bit.App.Pages
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CleanUp()
|
||||
private async void StartCiphersTotpTick()
|
||||
{
|
||||
_totpInterval = null;
|
||||
try
|
||||
{
|
||||
await _totpTickHelper.GenerateNewTotpValues();
|
||||
TotpSec = _totpTickHelper.TotpSec;
|
||||
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopCiphersTotpTick()
|
||||
{
|
||||
_totpTickCancellationToken?.Cancel();
|
||||
if (_totpTickTask != null)
|
||||
{
|
||||
await _totpTickTask;
|
||||
}
|
||||
}
|
||||
|
||||
public async void TogglePassword()
|
||||
@ -592,7 +613,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else if (id == "LoginTotp")
|
||||
{
|
||||
text = _totpCode;
|
||||
text = TotpCodeFormatted.Replace(" ", string.Empty);
|
||||
name = AppResources.VerificationCodeTotp;
|
||||
}
|
||||
else if (id == "LoginUri")
|
||||
|
@ -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}"
|
||||
@ -53,6 +54,14 @@
|
||||
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="authenticatorTemplate"
|
||||
x:DataType="pages:GroupingsPageTOTPListItem">
|
||||
<controls:AuthenticatorViewCell
|
||||
Cipher="{Binding Cipher}"
|
||||
WebsiteIconsEnabled="{Binding BindingContext.WebsiteIconsEnabled, Source={x:Reference _page}}"
|
||||
TotpSec="{Binding TotpSec}"/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="groupTemplate"
|
||||
x:DataType="pages:GroupingsPageListItem">
|
||||
<controls:ExtendedStackLayout Orientation="Horizontal"
|
||||
@ -104,6 +113,7 @@
|
||||
<pages:GroupingsPageListItemSelector x:Key="listItemDataTemplateSelector"
|
||||
HeaderTemplate="{StaticResource headerTemplate}"
|
||||
CipherTemplate="{StaticResource cipherTemplate}"
|
||||
AuthenticatorTemplate="{StaticResource authenticatorTemplate}"
|
||||
GroupTemplate="{StaticResource groupTemplate}" />
|
||||
|
||||
<StackLayout x:Key="mainLayout" x:Name="_mainLayout">
|
||||
@ -130,7 +140,6 @@
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Filter}" />
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Padding="20, 0"
|
||||
|
@ -22,6 +22,7 @@ namespace Bit.App.Pages
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly GroupingsPageViewModel _vm;
|
||||
private readonly string _pageName;
|
||||
|
||||
@ -29,7 +30,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public GroupingsPage(bool mainPage, CipherType? type = null, string folderId = null,
|
||||
string collectionId = null, string pageTitle = null, string vaultFilterSelection = null,
|
||||
PreviousPageInfo previousPage = null, bool deleted = false)
|
||||
PreviousPageInfo previousPage = null, bool deleted = false, bool showTotp = false)
|
||||
{
|
||||
_pageName = string.Concat(nameof(GroupingsPage), "_", DateTime.UtcNow.Ticks);
|
||||
InitializeComponent();
|
||||
@ -41,6 +42,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_vm = BindingContext as GroupingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.MainPage = mainPage;
|
||||
@ -48,6 +50,7 @@ namespace Bit.App.Pages
|
||||
_vm.FolderId = folderId;
|
||||
_vm.CollectionId = collectionId;
|
||||
_vm.Deleted = deleted;
|
||||
_vm.ShowTotp = showTotp;
|
||||
_previousPage = previousPage;
|
||||
if (pageTitle != null)
|
||||
{
|
||||
@ -69,7 +72,7 @@ namespace Bit.App.Pages
|
||||
ToolbarItems.Add(_lockItem);
|
||||
ToolbarItems.Add(_exitItem);
|
||||
}
|
||||
if (deleted)
|
||||
if (deleted || showTotp)
|
||||
{
|
||||
_absLayout.Children.Remove(_fab);
|
||||
ToolbarItems.Remove(_addItem);
|
||||
@ -189,10 +192,11 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
protected override async void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_vm.StopCiphersTotpTick().FireAndForget();
|
||||
_broadcasterService.Unsubscribe(_pageName);
|
||||
_vm.DisableRefreshing();
|
||||
_accountAvatar?.OnDisappearing();
|
||||
@ -200,35 +204,54 @@ namespace Bit.App.Pages
|
||||
|
||||
private async void RowSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
try
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
((ExtendedCollectionView)sender).SelectedItem = null;
|
||||
if (!DoOnce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.IsTrash)
|
||||
{
|
||||
await _vm.SelectTrashAsync();
|
||||
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
|
||||
{
|
||||
await _vm.SelectCipherAsync(totpItem.Cipher);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.IsTrash)
|
||||
{
|
||||
await _vm.SelectTrashAsync();
|
||||
}
|
||||
else if (item.IsTotpCode)
|
||||
{
|
||||
await _vm.SelectTotpCodesAsync();
|
||||
}
|
||||
else if (item.Cipher != null)
|
||||
{
|
||||
await _vm.SelectCipherAsync(item.Cipher);
|
||||
}
|
||||
else if (item.Folder != null)
|
||||
{
|
||||
await _vm.SelectFolderAsync(item.Folder);
|
||||
}
|
||||
else if (item.Collection != null)
|
||||
{
|
||||
await _vm.SelectCollectionAsync(item.Collection);
|
||||
}
|
||||
else if (item.Type != null)
|
||||
{
|
||||
await _vm.SelectTypeAsync(item.Type.Value);
|
||||
}
|
||||
}
|
||||
else if (item.Cipher != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _vm.SelectCipherAsync(item.Cipher);
|
||||
}
|
||||
else if (item.Folder != null)
|
||||
{
|
||||
await _vm.SelectFolderAsync(item.Folder);
|
||||
}
|
||||
else if (item.Collection != null)
|
||||
{
|
||||
await _vm.SelectCollectionAsync(item.Collection);
|
||||
}
|
||||
else if (item.Type != null)
|
||||
{
|
||||
await _vm.SelectTypeAsync(item.Type.Value);
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
_platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok).FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class GroupingsPageListGroup : List<GroupingsPageListItem>
|
||||
public class GroupingsPageListGroup : List<IGroupingsPageListItem>
|
||||
{
|
||||
public GroupingsPageListGroup(string name, int count, bool doUpper = true, bool first = false)
|
||||
: this(new List<GroupingsPageListItem>(), name, count, doUpper, first)
|
||||
: this(new List<IGroupingsPageListItem>(), name, count, doUpper, first)
|
||||
{ }
|
||||
|
||||
public GroupingsPageListGroup(List<GroupingsPageListItem> groupItems, string name, int count,
|
||||
public GroupingsPageListGroup(IEnumerable<IGroupingsPageListItem> groupItems, string name, int count,
|
||||
bool doUpper = true, bool first = false)
|
||||
{
|
||||
AddRange(groupItems);
|
||||
|
@ -17,6 +17,7 @@ namespace Bit.App.Pages
|
||||
public string ItemCount { get; set; }
|
||||
public bool FuzzyAutofill { get; set; }
|
||||
public bool IsTrash { get; set; }
|
||||
public bool IsTotpCode { get; set; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
@ -38,6 +39,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_name = Collection.Name;
|
||||
}
|
||||
else if (IsTotpCode)
|
||||
{
|
||||
_name = AppResources.VerificationCodes;
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
@ -82,6 +87,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_icon = BitwardenIcons.Collection;
|
||||
}
|
||||
else if (IsTotpCode)
|
||||
{
|
||||
_icon = BitwardenIcons.Clock;
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
switch (Type.Value)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
123
src/App/Pages/Vault/GroupingsPage/GroupingsPageTOTPListItem.cs
Normal file
123
src/App/Pages/Vault/GroupingsPage/GroupingsPageTOTPListItem.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.CommunityToolkit.ObjectModel;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
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;
|
||||
private CipherView _cipher;
|
||||
|
||||
private bool _websiteIconsEnabled;
|
||||
private string _iconImageSource = string.Empty;
|
||||
|
||||
public int interval { get; set; }
|
||||
private double _progress;
|
||||
private string _totpSec;
|
||||
private string _totpCodeFormatted;
|
||||
private TotpHelper _totpTickHelper;
|
||||
|
||||
|
||||
public GroupingsPageTOTPListItem(CipherView cipherView, bool websiteIconsEnabled)
|
||||
{
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||
|
||||
Cipher = cipherView;
|
||||
WebsiteIconsEnabled = websiteIconsEnabled;
|
||||
interval = _totpService.GetTimeInterval(Cipher.Login.Totp);
|
||||
CopyCommand = new AsyncCommand(CopyToClipboardAsync,
|
||||
onException: ex => _logger.Value.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
_totpTickHelper = new TotpHelper(cipherView);
|
||||
}
|
||||
|
||||
public AsyncCommand CopyCommand { get; set; }
|
||||
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => _cipher;
|
||||
set => SetProperty(ref _cipher, value);
|
||||
}
|
||||
|
||||
public string TotpCodeFormatted
|
||||
{
|
||||
get => _totpCodeFormatted;
|
||||
set => SetProperty(ref _totpCodeFormatted, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(TotpCodeFormattedStart),
|
||||
nameof(TotpCodeFormattedEnd),
|
||||
});
|
||||
}
|
||||
|
||||
public string TotpSec
|
||||
{
|
||||
get => _totpSec;
|
||||
set => SetProperty(ref _totpSec, value);
|
||||
}
|
||||
public double Progress
|
||||
{
|
||||
get => _progress;
|
||||
set => SetProperty(ref _progress, 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 string TotpCodeFormattedStart => TotpCodeFormatted?.Split(' ')[0];
|
||||
|
||||
public string TotpCodeFormattedEnd => TotpCodeFormatted?.Split(' ')[1];
|
||||
|
||||
public async Task CopyToClipboardAsync()
|
||||
{
|
||||
await _clipboardService.CopyTextAsync(TotpCodeFormatted);
|
||||
_platformUtilsService.ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, AppResources.VerificationCodeTotp));
|
||||
}
|
||||
|
||||
public async Task TotpTickAsync()
|
||||
{
|
||||
await _totpTickHelper.GenerateNewTotpValues();
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
TotpSec = _totpTickHelper.TotpSec;
|
||||
Progress = _totpTickHelper.Progress;
|
||||
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Resources;
|
||||
@ -29,13 +31,16 @@ namespace Bit.App.Pages
|
||||
private bool _showList;
|
||||
private bool _websiteIconsEnabled;
|
||||
private bool _syncRefreshing;
|
||||
private bool _showTotpFilter;
|
||||
private bool _totpFilterEnable;
|
||||
private string _noDataText;
|
||||
private List<CipherView> _allCiphers;
|
||||
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
|
||||
private Dictionary<string, int> _collectionCounts = new Dictionary<string, int>();
|
||||
private Dictionary<CipherType, int> _typeCounts = new Dictionary<CipherType, int>();
|
||||
private int _deletedCount = 0;
|
||||
|
||||
private CancellationTokenSource _totpTickCts;
|
||||
private Task _totpTickTask;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICollectionService _collectionService;
|
||||
@ -74,6 +79,9 @@ namespace Bit.App.Pages
|
||||
await LoadAsync();
|
||||
});
|
||||
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
|
||||
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
|
||||
onException: ex => _logger.Exception(ex),
|
||||
allowsMultipleExecutions: false);
|
||||
|
||||
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
|
||||
{
|
||||
@ -94,6 +102,7 @@ namespace Bit.App.Pages
|
||||
&& NoFolderCiphers.Count < NoFolderListSize
|
||||
&& (Collections is null || !Collections.Any());
|
||||
public List<CipherView> Ciphers { get; set; }
|
||||
public List<CipherView> TOTPCiphers { get; set; }
|
||||
public List<CipherView> FavoriteCiphers { get; set; }
|
||||
public List<CipherView> NoFolderCiphers { get; set; }
|
||||
public List<FolderView> Folders { get; set; }
|
||||
@ -151,9 +160,12 @@ namespace Bit.App.Pages
|
||||
get => _websiteIconsEnabled;
|
||||
set => SetProperty(ref _websiteIconsEnabled, value);
|
||||
}
|
||||
|
||||
public bool ShowTotp
|
||||
{
|
||||
get => _showTotpFilter;
|
||||
set => SetProperty(ref _showTotpFilter, value);
|
||||
}
|
||||
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
|
||||
|
||||
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
|
||||
public Command RefreshCommand { get; set; }
|
||||
public Command<CipherView> CipherOptionsCommand { get; set; }
|
||||
@ -188,13 +200,14 @@ namespace Bit.App.Pages
|
||||
{
|
||||
PageTitle = ShowVaultFilter ? AppResources.Vaults : AppResources.MyVault;
|
||||
}
|
||||
|
||||
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
_doingLoad = true;
|
||||
LoadedOnce = true;
|
||||
ShowNoData = false;
|
||||
Loading = true;
|
||||
ShowList = false;
|
||||
ShowAddCipherButton = !Deleted;
|
||||
|
||||
var groupedItems = new List<GroupingsPageListGroup>();
|
||||
var page = Page as GroupingsPage;
|
||||
|
||||
@ -218,6 +231,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (MainPage)
|
||||
{
|
||||
AddTotpGroupItem(canAccessPremium, groupedItems, uppercaseGroupNames);
|
||||
|
||||
groupedItems.Add(new GroupingsPageListGroup(
|
||||
AppResources.Types, 4, uppercaseGroupNames, !hasFavorites)
|
||||
{
|
||||
@ -274,10 +289,12 @@ 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()));
|
||||
CreateCipherGroupedItems(groupedItems);
|
||||
}
|
||||
if (ShowTotp && (!TOTPCiphers?.Any() ?? false))
|
||||
{
|
||||
Page.Navigation.PopAsync();
|
||||
return;
|
||||
}
|
||||
if (ShowNoFolderCipherGroup)
|
||||
{
|
||||
@ -365,6 +382,60 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTotpGroupItem(bool canAccessPremium, List<GroupingsPageListGroup> groupedItems, bool uppercaseGroupNames)
|
||||
{
|
||||
if (canAccessPremium && TOTPCiphers?.Any() == true)
|
||||
{
|
||||
groupedItems.Insert(0, new GroupingsPageListGroup(
|
||||
AppResources.Totp, 1, uppercaseGroupNames, false)
|
||||
{
|
||||
new GroupingsPageListItem
|
||||
{
|
||||
IsTotpCode = true,
|
||||
Type = CipherType.Login,
|
||||
ItemCount = TOTPCiphers.Count().ToString("N0")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
|
||||
{
|
||||
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
|
||||
_totpTickCts?.Cancel();
|
||||
if (ShowTotp)
|
||||
{
|
||||
var ciphersListItems = TOTPCiphers.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)
|
||||
{
|
||||
_totpTickCts?.Cancel();
|
||||
_totpTickCts = new CancellationTokenSource();
|
||||
_totpTickTask = new TimerTask(logger, () => ciphersListItems.ForEach(i => i.TotpTickAsync()), _totpTickCts).RunPeriodic();
|
||||
}
|
||||
|
||||
public async Task StopCiphersTotpTick()
|
||||
{
|
||||
_totpTickCts?.Cancel();
|
||||
if (_totpTickTask != null)
|
||||
{
|
||||
await _totpTickTask;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableRefreshing()
|
||||
{
|
||||
Refreshing = false;
|
||||
@ -425,6 +496,13 @@ namespace Bit.App.Pages
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task SelectTotpCodesAsync()
|
||||
{
|
||||
var page = new GroupingsPage(false, CipherType.Login, null, null, AppResources.VerificationCodes, _vaultFilterSelection, null,
|
||||
false, true);
|
||||
await Page.Navigation.PushAsync(page);
|
||||
}
|
||||
|
||||
public async Task ExitAsync()
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.ExitConfirmation,
|
||||
@ -462,6 +540,7 @@ namespace Bit.App.Pages
|
||||
NoDataText = AppResources.NoItems;
|
||||
_allCiphers = await GetAllCiphersAsync();
|
||||
HasCiphers = _allCiphers.Any();
|
||||
TOTPCiphers = _allCiphers.Where(c => c.IsDeleted == Deleted && c.Type == CipherType.Login && !string.IsNullOrEmpty(c.Login?.Totp)).ToList();
|
||||
FavoriteCiphers?.Clear();
|
||||
NoFolderCiphers?.Clear();
|
||||
_folderCounts.Clear();
|
||||
@ -487,6 +566,10 @@ namespace Bit.App.Pages
|
||||
Filter = c => c.IsDeleted;
|
||||
NoDataText = AppResources.NoItemsTrash;
|
||||
}
|
||||
else if (ShowTotp)
|
||||
{
|
||||
Filter = c => c.Type == CipherType.Login && !c.IsDeleted && !string.IsNullOrEmpty(c.Login?.Totp);
|
||||
}
|
||||
else if (Type != null)
|
||||
{
|
||||
Filter = c => c.Type == Type.Value && !c.IsDeleted;
|
||||
|
@ -1,13 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.ScanPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:forms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
|
||||
xmlns:core="clr-namespace:Bit.Core;assembly=BitwardenCore"
|
||||
xmlns:zxing="clr-namespace:ZXing.Net.Mobile.Forms;assembly=ZXing.Net.Mobile.Forms"
|
||||
x:Name="_page"
|
||||
Title="{u:I18n ScanQrTitle}">
|
||||
Title="{Binding ScanQrPageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:ScanPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
@ -16,67 +29,114 @@
|
||||
<Grid
|
||||
VerticalOptions="FillAndExpand"
|
||||
HorizontalOptions="FillAndExpand">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<zxing:ZXingScannerView
|
||||
x:Name="_zxing"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="FillAndExpand"
|
||||
AutomationId="zxingScannerView"
|
||||
OnScanResult="OnScanResult">
|
||||
</zxing:ZXingScannerView>
|
||||
|
||||
<Grid
|
||||
VerticalOptions="FillAndExpand"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
OnScanResult="OnScanResult"/>
|
||||
<StackLayout
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
AutomationId="zxingDefaultOverlay">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="30,0">
|
||||
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
VerticalOptions="Fill"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Black"
|
||||
Opacity="0.7" />
|
||||
|
||||
<Label
|
||||
Text="{u:I18n CameraInstructionTop}"
|
||||
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
VerticalOptions="Center"
|
||||
<forms:SKCanvasView
|
||||
x:Name="SkCanvasView"
|
||||
Margin="0,50,0,0"
|
||||
WidthRequest="250"
|
||||
HeightRequest="250"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="White" />
|
||||
PaintSurface="OnCanvasViewPaintSurface"/>
|
||||
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
VerticalOptions="Fill"
|
||||
<controls:IconButton
|
||||
x:Name="_checkIcon"
|
||||
IsVisible="{Binding ShowScanner}"
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding Source={x:Static core:BitwardenIcons.CheckCircle}}"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Start"
|
||||
FontSize="Title"
|
||||
TextColor="Transparent"/>
|
||||
</StackLayout>
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
|
||||
BackgroundColor="{DynamicResource BackgroundColor}"/>
|
||||
<StackLayout
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Transparent" />
|
||||
|
||||
<BoxView
|
||||
IsVisible="{Binding ShowScanner, Converter={StaticResource inverseBool}}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Margin="30,0">
|
||||
<Label
|
||||
Text="{u:I18n EnterKeyManually}"
|
||||
FontSize="Title" />
|
||||
<Label
|
||||
Text="{u:I18n AuthenticatorKeyScanner}"
|
||||
StyleClass="box-label" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_authenticationKeyEntry"
|
||||
Text="{Binding TotpAuthenticationKey}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
StyleClass="box-value" />
|
||||
<Button
|
||||
Text="{u:I18n AddTotp}"
|
||||
StyleClass="box-button-row"
|
||||
Clicked="AddAuthenticationKey_OnClicked"/>
|
||||
</StackLayout>
|
||||
<BoxView
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
VerticalOptions="Fill"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
BackgroundColor="Black"
|
||||
Opacity="0.7" />
|
||||
|
||||
<StackLayout
|
||||
VerticalOptions="Start"
|
||||
HorizontalOptions="Center"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2">
|
||||
<Label
|
||||
Text="{u:I18n CameraInstructionBottom}"
|
||||
AutomationId="zxingDefaultOverlay_BottomTextLabel"
|
||||
Text="{Binding CameraInstructionTop}"
|
||||
AutomationId="zxingDefaultOverlay_TopTextLabel"
|
||||
Margin="30,15,30,0"
|
||||
HorizontalOptions="Center"
|
||||
HorizontalTextAlignment="Center"
|
||||
StyleClass="text-sm"
|
||||
TextColor="White" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
FormattedText="{Binding ToggleScanModeLabel}"
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="White" />
|
||||
</Grid>
|
||||
|
||||
Margin="0,15"
|
||||
StyleClass="text-sm"
|
||||
FontAttributes="Bold"
|
||||
VerticalOptions="End"
|
||||
HorizontalOptions="Center" >
|
||||
<Label.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="ToggleScanMode_OnTapped" />
|
||||
</Label.GestureRecognizers>
|
||||
</Label>
|
||||
</Grid>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
</pages:BaseContentPage>
|
@ -1,19 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Forms;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class ScanPage : BaseContentPage
|
||||
{
|
||||
private ScanPageViewModel ViewModel => BindingContext as ScanPageViewModel;
|
||||
private readonly Action<string> _callback;
|
||||
|
||||
private CancellationTokenSource _autofocusCts;
|
||||
private Task _continuousAutofocusTask;
|
||||
private readonly Color _greenColor;
|
||||
private readonly SKColor _blueSKColor;
|
||||
private readonly SKColor _greenSKColor;
|
||||
private readonly Stopwatch _stopwatch;
|
||||
private bool _pageIsActive;
|
||||
private bool _qrcodeFound;
|
||||
private float _scale;
|
||||
|
||||
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
|
||||
@ -32,6 +44,12 @@ namespace Bit.App.Pages
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
_greenColor = ThemeManager.GetResourceColor("SuccessColor");
|
||||
_greenSKColor = _greenColor.ToSKColor();
|
||||
_blueSKColor = ThemeManager.GetResourceColor("PrimaryColor").ToSKColor();
|
||||
_stopwatch = new Stopwatch();
|
||||
_qrcodeFound = false;
|
||||
}
|
||||
|
||||
protected override void OnAppearing()
|
||||
@ -58,7 +76,14 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (!autofocusCts.IsCancellationRequested)
|
||||
{
|
||||
_zxing.AutoFocus();
|
||||
try
|
||||
{
|
||||
_zxing.AutoFocus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -69,27 +94,83 @@ namespace Bit.App.Pages
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}, autofocusCts.Token);
|
||||
_pageIsActive = true;
|
||||
AnimationLoopAsync();
|
||||
}
|
||||
|
||||
protected override async void OnDisappearing()
|
||||
{
|
||||
_autofocusCts?.Cancel();
|
||||
|
||||
if (_continuousAutofocusTask != null)
|
||||
{
|
||||
await _continuousAutofocusTask;
|
||||
}
|
||||
_zxing.IsScanning = false;
|
||||
|
||||
_pageIsActive = false;
|
||||
base.OnDisappearing();
|
||||
}
|
||||
|
||||
private void OnScanResult(ZXing.Result result)
|
||||
private async void OnScanResult(ZXing.Result result)
|
||||
{
|
||||
// Stop analysis until we navigate away so we don't keep reading barcodes
|
||||
_zxing.IsAnalyzing = false;
|
||||
try
|
||||
{
|
||||
// Stop analysis until we navigate away so we don't keep reading barcodes
|
||||
_zxing.IsAnalyzing = false;
|
||||
var text = result?.Text;
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
if (text.StartsWith("otpauth://totp"))
|
||||
{
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task QrCodeFoundAsync()
|
||||
{
|
||||
_qrcodeFound = true;
|
||||
Vibration.Vibrate();
|
||||
await Task.Delay(1000);
|
||||
_zxing.IsScanning = false;
|
||||
var text = result?.Text;
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAuthenticationKey_OnClicked(object sender, EventArgs e)
|
||||
{
|
||||
var text = ViewModel.TotpAuthenticationKey;
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
if (text.StartsWith("otpauth://totp"))
|
||||
@ -98,7 +179,7 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
|
||||
!string.IsNullOrWhiteSpace(uri?.Query))
|
||||
!string.IsNullOrWhiteSpace(uri?.Query))
|
||||
{
|
||||
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
|
||||
foreach (var part in queryParts)
|
||||
@ -114,11 +195,77 @@ namespace Bit.App.Pages
|
||||
_callback(null);
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private void ToggleScanMode_OnTapped(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
ViewModel.ToggleScanModeCommand.Execute(null);
|
||||
if (!ViewModel.ShowScanner)
|
||||
{
|
||||
await Navigation.PopModalAsync();
|
||||
_authenticationKeyEntry.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
|
||||
{
|
||||
var info = args.Info;
|
||||
var surface = args.Surface;
|
||||
var canvas = surface.Canvas;
|
||||
var margins = 20;
|
||||
var maxSquareSize = (Math.Min(info.Height, info.Width) * 0.9f - margins) * _scale;
|
||||
var squareSize = maxSquareSize;
|
||||
var lineSize = squareSize * 0.15f;
|
||||
var startXPoint = (info.Width / 2) - (squareSize / 2);
|
||||
var startYPoint = (info.Height / 2) - (squareSize / 2);
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
using (var strokePaint = new SKPaint
|
||||
{
|
||||
Color = _qrcodeFound ? _greenSKColor : _blueSKColor,
|
||||
StrokeWidth = 9 * _scale,
|
||||
StrokeCap = SKStrokeCap.Round,
|
||||
})
|
||||
{
|
||||
canvas.Scale(1, 1);
|
||||
//top left
|
||||
canvas.DrawLine(startXPoint, startYPoint, startXPoint, startYPoint + lineSize, strokePaint);
|
||||
canvas.DrawLine(startXPoint, startYPoint, startXPoint + lineSize, startYPoint, strokePaint);
|
||||
//bot left
|
||||
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint, startYPoint + squareSize - lineSize, strokePaint);
|
||||
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint + lineSize, startYPoint + squareSize, strokePaint);
|
||||
//top right
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize - lineSize, startYPoint, strokePaint);
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize, startYPoint + lineSize, strokePaint);
|
||||
//bot right
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize - lineSize, startYPoint + squareSize, strokePaint);
|
||||
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize, startYPoint + squareSize - lineSize, strokePaint);
|
||||
}
|
||||
}
|
||||
|
||||
async Task AnimationLoopAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_stopwatch.Start();
|
||||
while (_pageIsActive)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_checkIcon.TextColor = _greenColor;
|
||||
SkCanvasView.InvalidateSurface();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Value?.Exception(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stopwatch?.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
src/App/Pages/Vault/ScanPageViewModel.cs
Normal file
61
src/App/Pages/Vault/ScanPageViewModel.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class ScanPageViewModel : BaseViewModel
|
||||
{
|
||||
private bool _showScanner = true;
|
||||
private string _totpAuthenticationKey;
|
||||
|
||||
public ScanPageViewModel()
|
||||
{
|
||||
ToggleScanModeCommand = new Command(() => ShowScanner = !ShowScanner);
|
||||
}
|
||||
|
||||
public Command ToggleScanModeCommand { get; set; }
|
||||
public string ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner;
|
||||
public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered;
|
||||
public string TotpAuthenticationKey
|
||||
{
|
||||
get => _totpAuthenticationKey;
|
||||
set => SetProperty(ref _totpAuthenticationKey, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ToggleScanModeLabel)
|
||||
});
|
||||
}
|
||||
public bool ShowScanner
|
||||
{
|
||||
get => _showScanner;
|
||||
set => SetProperty(ref _showScanner, value,
|
||||
additionalPropertyNames: new string[]
|
||||
{
|
||||
nameof(ToggleScanModeLabel),
|
||||
nameof(ScanQrPageTitle),
|
||||
nameof(CameraInstructionTop)
|
||||
});
|
||||
}
|
||||
|
||||
public FormattedString ToggleScanModeLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
var fs = new FormattedString();
|
||||
fs.Spans.Add(new Span
|
||||
{
|
||||
Text = ShowScanner ? AppResources.CannotScanQRCode : AppResources.CannotAddAuthenticatorKey,
|
||||
TextColor = ThemeManager.GetResourceColor("TitleTextColor")
|
||||
});
|
||||
fs.Spans.Add(new Span
|
||||
{
|
||||
Text = ShowScanner ? AppResources.EnterKeyManually : AppResources.ScanQRCode,
|
||||
TextColor = ThemeManager.GetResourceColor("ScanningToggleModeTextColor")
|
||||
});
|
||||
return fs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
src/App/Resources/AppResources.Designer.cs
generated
88
src/App/Resources/AppResources.Designer.cs
generated
@ -353,6 +353,12 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string Authenticator {
|
||||
get {
|
||||
return ResourceManager.GetString("Authenticator", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Name", resourceCulture);
|
||||
@ -1457,15 +1463,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4049,6 +4049,78 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
public static string Totp {
|
||||
get {
|
||||
return ResourceManager.GetString("Totp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string VerificationCodes {
|
||||
get {
|
||||
return ResourceManager.GetString("VerificationCodes", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PremiumSubscriptionRequired {
|
||||
get {
|
||||
return ResourceManager.GetString("PremiumSubscriptionRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CannotAddAuthenticatorKey {
|
||||
get {
|
||||
return ResourceManager.GetString("CannotAddAuthenticatorKey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ScanQRCode {
|
||||
get {
|
||||
return ResourceManager.GetString("ScanQRCode", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CannotScanQRCode {
|
||||
get {
|
||||
return ResourceManager.GetString("CannotScanQRCode", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AuthenticatorKeyScanner {
|
||||
get {
|
||||
return ResourceManager.GetString("AuthenticatorKeyScanner", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string EnterKeyManually {
|
||||
get {
|
||||
return ResourceManager.GetString("EnterKeyManually", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string AddTotp {
|
||||
get {
|
||||
return ResourceManager.GetString("AddTotp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SetupTotp {
|
||||
get {
|
||||
return ResourceManager.GetString("SetupTotp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string OnceTheKeyIsSuccessfullyEntered {
|
||||
get {
|
||||
return ResourceManager.GetString("OnceTheKeyIsSuccessfullyEntered", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SelectAddTotpToStoreTheKeySafely {
|
||||
get {
|
||||
return ResourceManager.GetString("SelectAddTotpToStoreTheKeySafely", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string NeverLockWarning {
|
||||
get {
|
||||
return ResourceManager.GetString("NeverLockWarning", resourceCulture);
|
||||
|
@ -299,6 +299,10 @@
|
||||
<value>My Vault</value>
|
||||
<comment>The title for the vault page.</comment>
|
||||
</data>
|
||||
<data name="Authenticator" xml:space="preserve">
|
||||
<value>Authenticator</value>
|
||||
<comment>Authenticator TOTP feature</comment>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
<comment>Label for an entity name.</comment>
|
||||
@ -895,11 +899,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>
|
||||
@ -2260,6 +2262,43 @@
|
||||
<data name="All" xml:space="preserve">
|
||||
<value>All</value>
|
||||
</data>
|
||||
<data name="Totp" xml:space="preserve">
|
||||
<value>TOTP</value>
|
||||
</data>
|
||||
<data name="VerificationCodes" xml:space="preserve">
|
||||
<value>Verification Codes</value>
|
||||
</data>
|
||||
<data name="PremiumSubscriptionRequired" xml:space="preserve">
|
||||
<value>Premium subscription required</value>
|
||||
</data>
|
||||
<data name="CannotAddAuthenticatorKey" xml:space="preserve">
|
||||
<value>Cannot add authenticator key? </value>
|
||||
</data>
|
||||
<data name="ScanQRCode" xml:space="preserve">
|
||||
<value>Scan QR Code</value>
|
||||
</data>
|
||||
<data name="CannotScanQRCode" xml:space="preserve">
|
||||
<value>Cannot scan QR Code? </value>
|
||||
</data>
|
||||
<data name="AuthenticatorKeyScanner" xml:space="preserve">
|
||||
<value>Authenticator Key</value>
|
||||
</data>
|
||||
<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">
|
||||
<value>Set up TOTP</value>
|
||||
</data>
|
||||
<data name="OnceTheKeyIsSuccessfullyEntered" xml:space="preserve">
|
||||
<value>Once the key is successfully entered,
|
||||
select Add TOTP to store the key safely</value>
|
||||
</data>
|
||||
<data name="SelectAddTotpToStoreTheKeySafely" xml:space="preserve">
|
||||
<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>
|
||||
|
@ -79,9 +79,34 @@
|
||||
<Setter Property="StepperForegroundColor"
|
||||
Value="{DynamicResource StepperForegroundColor}" />
|
||||
</Style>
|
||||
<Style TargetType="Frame"
|
||||
Class="btn-icon-row">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||
<Setter Property="BorderColor"
|
||||
Value="{DynamicResource ButtonBorderColor}" />
|
||||
<Setter Property="CornerRadius"
|
||||
Value="5" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 5, 0, 0" />
|
||||
<Setter Property="HasShadow"
|
||||
Value="False" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Buttons -->
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||
|
@ -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>
|
||||
|
@ -71,4 +71,6 @@
|
||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
@ -71,4 +71,6 @@
|
||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
@ -71,4 +71,6 @@
|
||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#175DDC</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
@ -71,4 +71,6 @@
|
||||
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
|
||||
|
||||
<Color x:Key="HyperlinkColor">#81a1c1</Color>
|
||||
|
||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||
</ResourceDictionary>
|
||||
|
@ -92,6 +92,32 @@
|
||||
<Setter Property="StepperForegroundColor"
|
||||
Value="{DynamicResource StepperForegroundColor}" />
|
||||
</Style>
|
||||
<Style TargetType="Frame"
|
||||
Class="btn-icon-row">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColor}" />
|
||||
<Setter Property="BorderColor"
|
||||
Value="{DynamicResource ButtonBorderColor}" />
|
||||
<Setter Property="CornerRadius"
|
||||
Value="5" />
|
||||
<Setter Property="Margin"
|
||||
Value="0, 10, 0, 0" />
|
||||
<Setter Property="HasShadow"
|
||||
Value="False" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{DynamicResource ButtonBackgroundColorDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Buttons -->
|
||||
|
||||
|
56
src/App/Utilities/TimerTask.cs
Normal file
56
src/App/Utilities/TimerTask.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Utilities
|
||||
{
|
||||
public class TimerTask
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly Action _action;
|
||||
private readonly CancellationTokenSource _cancellationToken;
|
||||
|
||||
public TimerTask(ILogger logger, Action action, CancellationTokenSource cancellationToken)
|
||||
{
|
||||
_logger = logger;
|
||||
_action = action ?? throw new ArgumentNullException();
|
||||
_cancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public Task RunPeriodic(TimeSpan? interval = null)
|
||||
{
|
||||
interval = interval ?? TimeSpan.FromSeconds(1);
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Device.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
if (!_cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Exception(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Task.Delay(interval.Value, _cancellationToken.Token);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Exception(ex);
|
||||
}
|
||||
}, _cancellationToken.Token);
|
||||
}
|
||||
}
|
||||
}
|
57
src/App/Utilities/TotpHelper.cs
Normal file
57
src/App/Utilities/TotpHelper.cs
Normal file
@ -0,0 +1,57 @@
|
||||
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 CipherView _cipher;
|
||||
private int _interval;
|
||||
|
||||
public TotpHelper(CipherView cipher)
|
||||
{
|
||||
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
_cipher = cipher;
|
||||
_interval = _totpService.GetTimeInterval(cipher?.Login?.Totp);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (totpCode.Length <= 4)
|
||||
{
|
||||
return totpCode;
|
||||
}
|
||||
|
||||
var half = (int)Math.Floor(totpCode.Length / 2M);
|
||||
return string.Format("{0} {1}", totpCode.Substring(0, half),
|
||||
totpCode.Substring(half));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user