1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

...

57 Commits

Author SHA1 Message Date
André Bispo
bcc260183e [PS-416] Fixed a bug where the item details page was not updating after saving. 2022-07-26 21:28:32 +01:00
André Bispo
2f2b90acee [SSG-416] PR fixes 2022-07-18 22:13:44 +01:00
André Bispo
1b82249d44 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Resources/AppResources.Designer.cs
#	src/App/Resources/AppResources.resx
2022-07-18 14:21:15 +01:00
André Bispo
f432a58e9f [SSG-416] run dotnet tool run dotnet-format 2022-07-13 12:53:54 +01:00
André Bispo
7e6935f21c [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 2022-07-13 11:13:50 +01:00
André Bispo
7cf34b845e [SSG-416] Mobile PR Fixes 2022-07-12 23:00:31 +01:00
André Bispo
8ed909eb91 [SG-416] Updated scanner mode toggle text color to be inline with figma designs 2022-07-07 17:58:39 +01:00
André Bispo
a57dc50a50 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Resources/AppResources.Designer.cs
#	src/App/Resources/AppResources.resx
2022-07-07 17:13:04 +01:00
André Bispo
9074f533f9 Merge branch 'master' into feature/totp-tab 2022-07-07 15:33:08 +01:00
André Bispo
9499fa0fb3 [SSG-416] Updated ViewPageViewModel and code refactoring. 2022-07-07 15:27:44 +01:00
André Bispo
69e47b651d [SSG-416] removed unnecessary using. 2022-07-06 19:50:09 +01:00
André Bispo
dbe72c68a8 [SSG-416] removed unnecessary changes to the TabsPage file 2022-07-06 19:36:12 +01:00
André Bispo
3822373b98 [SSG-416] Removed the dashes from free user and just left the Premium subscription required. 2022-07-06 18:00:05 +01:00
André Bispo
a51c2efdae PS-70 changed premium required label. fixed bug when to show premium required label. 2022-06-29 11:29:29 +01:00
André Bispo
0ce01ddd84 PS-70 PR fix for AppResource vs code behind generation. 2022-06-28 12:51:45 +01:00
André Bispo
6b33266721 PS-70 removed update to premium uri launch 2022-06-24 19:50:21 +01:00
André Bispo
6f61f1f4d8 PS-70 code format 2022-06-24 19:31:43 +01:00
André Bispo
d3d935fff6 Merge branch 'master' into feature/totp-tab
# Conflicts:
#	src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs
#	src/App/Pages/Vault/ScanPage.xaml.cs
2022-06-24 17:39:24 +01:00
André Bispo
a97abc61d5 PS-70 removed shadow for newer versions of android 2022-06-24 16:17:25 +01:00
André Bispo
6e30ec2dc7 PS-70 fixed font clipping bug on iOS 2022-06-24 12:19:04 +01:00
André Bispo
472f251714 PS-70 Added frame styling for iOS, since frame view has different base configuration for android and iOS. 2022-06-24 11:57:57 +01:00
André Bispo
a6069ef37b 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. 2022-06-23 17:17:59 +01:00
André Bispo
6aeec4a89a PS-70 Added label on top of button to solve UI bug. 2022-06-23 11:31:50 +01:00
André Bispo
9ddbe400f8 PS-70 changed labels on manual scanner screen 2022-06-22 19:02:16 +01:00
André Bispo
9bc8e6912d PS-70 added copy button to totp edit cipher. Added row button when totp is null. 2022-06-22 19:01:23 +01:00
André Bispo
64beb23239 PS-70 fixed totp cell title label font 2022-06-22 16:12:22 +01:00
André Bispo
94f2c5a4f8 PS-70 added vibrate permission to manifest. refactored scanpage code. added manual authentication key feature in scanner. 2022-06-22 12:26:51 +01:00
André Bispo
3522953b8c PS-70 fixed scanner animation for android devices 2022-06-22 10:57:39 +01:00
André Bispo
3a91bc7eb1 PS-70 let zxing scanner camera feed on until screen is closed. 2022-06-22 10:13:58 +01:00
André Bispo
277dd5942f PS-70 Added scanner square corner overlay. Added scanning animation. Added scan success animation. 2022-06-21 22:28:13 +01:00
André Bispo
46c73dafbe PS-70 Splited totp code to adjust spacing. 2022-06-21 17:07:45 +01:00
André Bispo
c564a34317 PS-70 added new UI to enter code manually in the QR Code scanner screen. Changed existing labels on scanner screen. 2022-06-20 18:41:50 +01:00
André Bispo
e606ff88a4 PS-70 Renamed TOTP to Totp to have consistency in naming. Removed a11y text of switch because android was overlapping text. 2022-06-20 14:42:33 +01:00
André Bispo
67fbd55ae9 PS-70 added text labels to resource files 2022-06-20 12:39:53 +01:00
André Bispo
c67229d235 PS-70 show upgrade to premium text on details to free user. 2022-06-20 12:28:00 +01:00
André Bispo
e8d31b8a22 PS-70 add copy to clipboard. 2022-06-20 12:27:11 +01:00
André Bispo
3b763e454c PS-70 removed unnecessary code 2022-06-20 11:18:15 +01:00
André Bispo
c9c3e6d98d PS-70 show toggle only if it's premium 2022-06-20 11:17:20 +01:00
André Bispo
751fdf2db6 PS-70 Added new props to custom control. 2022-06-17 10:23:25 +01:00
André Bispo
6ae7d6dd8d PS-70 Fixed grid cell width. Added red progress at 20 percent. Refactored circular progress view. 2022-06-15 20:54:00 +01:00
André Bispo
a3218aed26 PS-70 Added circular progress to the OTP count down 2022-06-13 12:57:07 +01:00
André Bispo
77c8156515 fixed formatting 2022-06-07 19:06:26 +01:00
André Bispo
1c0375ef05 removed unnecessary code on vm 2022-06-07 18:59:17 +01:00
André Bispo
b3f2730c71 PS-70 removed old authentication tab 2022-06-07 18:44:07 +01:00
André Bispo
5e2142fba7 PS-70 Added toggle to quickly filter TOTP cypher items and show their details, Added new text resource 2022-06-07 18:38:48 +01:00
André Bispo
c91277bc43 Merge branch 'beeep-totp' into feature/totp-tab
# Conflicts:
#	src/App/Pages/Authenticator/AuthenticatorPage.xaml
#	src/App/Pages/Authenticator/AuthenticatorPage.xaml.cs
#	src/App/Pages/Authenticator/AuthenticatorPageViewModel.cs
2022-06-06 14:16:40 +01:00
Jacob Fink
a484ca633c Merge branch 'beeep-totp' of https://github.com/bitwarden/mobile into beeep-totp 2022-06-06 08:58:52 -04:00
André Bispo
31d1a2e083 enable context loading and vm init 2022-06-04 20:19:03 +01:00
André Bispo
655b51b6a5 Merge branch 'master' into beeep-totp
# Conflicts:
#	src/App/App.csproj
2022-06-04 18:05:14 +01:00
Jacob Fink
0b626cedc7 got the countdown working 2022-03-04 16:44:22 -05:00
Jacob Fink
3ac2580742 add toolbar icons 2022-02-17 11:26:14 -05:00
Jacob Fink
008ed8eb56 add authentication view cell 2022-02-16 15:55:02 -05:00
Jacob Fink
26e0e43bb4 Merge branch 'master' into beeep-totp 2022-02-16 09:25:29 -05:00
Jacob Fink
0ad992faec add tab page 2022-02-16 09:25:19 -05:00
Jacob Fink
98dd8298ea clear extra code and fix build 2022-02-15 14:15:42 -05:00
Jacob Fink
bb37bac620 Revert config files from previous commit
This reverts commit b02c58e362.
2022-02-15 13:36:32 -05:00
Carlos J. Muentes
b02c58e362 Initial commit of new TOTP page 2022-02-11 15:22:51 -05:00
29 changed files with 4980 additions and 5817 deletions

View File

@@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

View File

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

View File

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

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();
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

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

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
@@ -29,6 +30,7 @@ namespace Bit.App.Pages
private readonly IEventService _eventService;
private readonly IPolicyService _policyService;
private readonly ILogger _logger;
private readonly IClipboardService _clipboardService;
private CipherView _cipher;
private bool _showNotesSeparator;
@@ -53,6 +55,7 @@ namespace Bit.App.Pages
nameof(ShowUris),
nameof(ShowAttachments),
nameof(ShowCollections),
nameof(HasTotpValue)
};
private List<KeyValuePair<UriMatchType?, string>> _matchDetectionOptions =
new List<KeyValuePair<UriMatchType?, string>>
@@ -80,6 +83,7 @@ namespace Bit.App.Pages
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
_logger = ServiceContainer.Resolve<ILogger>("logger");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
@@ -89,6 +93,7 @@ namespace Bit.App.Pages
UriOptionsCommand = new Command<LoginUriView>(UriOptions);
FieldOptionsCommand = new Command<AddEditPageFieldViewModel>(FieldOptions);
PasswordPromptHelpCommand = new Command(PasswordPromptHelp);
CopyCommand = new AsyncCommand(CopyTotpClipboardAsync, onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
Uris = new ExtendedObservableCollection<LoginUriView>();
Fields = new ExtendedObservableCollection<AddEditPageFieldViewModel>();
Collections = new ExtendedObservableCollection<CollectionViewModel>();
@@ -150,6 +155,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; }
@@ -300,7 +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 => IsLogin && !string.IsNullOrEmpty(Cipher?.Login?.Totp);
public string SetupTotpText => $"{BitwardenIcons.Camera} {AppResources.SetupTotp}";
public void Init()
{
PageTitle = EditMode && !CloneMode ? AppResources.EditItem : AppResources.AddItem;
@@ -858,6 +865,19 @@ namespace Bit.App.Pages
await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe);
}
}
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 AddEditPageFieldViewModel : ExtendedViewModel

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}"
@@ -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">
@@ -131,6 +141,29 @@
AutomationProperties.Name="{u:I18n Filter}" />
</StackLayout>
<StackLayout
IsVisible="{Binding ShowTotpFilter}"
Orientation="Horizontal"
Margin="0,5,10,0">
<Label
Text="{u:I18n DisplayItemsContainingTOTP}"
LineBreakMode="TailTruncation"
Margin="10,0"
StyleClass="text-md, text-muted"
VerticalOptions="Center"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding TotpFilterEnable}"
StyleClass="box-value"
HorizontalOptions="End">
<Switch.Behaviors>
<xct:EventToCommandBehavior
EventName="Toggled"
Command="{Binding TotpFilterCommand}" />
</Switch.Behaviors>
</Switch>
</StackLayout>
<StackLayout
VerticalOptions="CenterAndExpand"
Padding="20, 0"

View File

@@ -189,10 +189,11 @@ namespace Bit.App.Pages
return false;
}
protected override void OnDisappearing()
protected override async void OnDisappearing()
{
base.OnDisappearing();
IsBusy = false;
_vm.StopCiphersTotpTick();
_broadcasterService.Unsubscribe(_pageName);
_vm.DisableRefreshing();
_accountAvatar?.OnDisappearing();
@@ -205,6 +206,13 @@ namespace Bit.App.Pages
{
return;
}
if (e.CurrentSelection?.FirstOrDefault() is GroupingsPageTOTPListItem totpItem)
{
await _vm.SelectCipherAsync(totpItem.Cipher);
return;
}
if (!(e.CurrentSelection?.FirstOrDefault() is GroupingsPageListItem item))
{
return;

View File

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

View File

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

View 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;
});
}
}
}

View File

@@ -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,12 @@ namespace Bit.App.Pages
await LoadAsync();
});
CipherOptionsCommand = new Command<CipherView>(CipherOptionsAsync);
VaultFilterCommand = new AsyncCommand(VaultFilterOptionsAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
TotpFilterCommand = new AsyncCommand(LoadAsync,
onException: ex => _logger.Exception(ex),
allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
{
@@ -90,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());
@@ -151,12 +165,21 @@ namespace Bit.App.Pages
get => _websiteIconsEnabled;
set => SetProperty(ref _websiteIconsEnabled, value);
}
public bool ShowTotpFilter
{
get => _showTotpFilter;
set => SetProperty(ref _showTotpFilter, value);
}
public bool TotpFilterEnable
{
get => _totpFilterEnable;
set => SetProperty(ref _totpFilterEnable, value);
}
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
public ObservableRangeCollection<IGroupingsPageListItem> GroupedItems { get; set; }
public Command RefreshCommand { get; set; }
public Command<CipherView> CipherOptionsCommand { get; set; }
public ICommand TotpFilterCommand { get; }
public bool LoadedOnce { get; set; }
public async Task LoadAsync()
@@ -188,13 +211,15 @@ 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;
ShowTotpFilter = Type == CipherType.Login && canAccessPremium;
var groupedItems = new List<GroupingsPageListGroup>();
var page = Page as GroupingsPage;
@@ -274,10 +299,7 @@ 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 (ShowNoFolderCipherGroup)
{
@@ -365,6 +387,44 @@ namespace Bit.App.Pages
}
}
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
{
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
_totpTickCts?.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)
{
_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;

View File

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

View File

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

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

View File

@@ -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.ViewPage"
@@ -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>
@@ -588,7 +609,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}"
@@ -636,7 +657,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}}"
@@ -700,4 +721,4 @@
</Button>
</AbsoluteLayout>
</pages:BaseContentPage>
</pages:BaseContentPage>

View File

@@ -112,7 +112,7 @@ namespace Bit.App.Pages
base.OnDisappearing();
IsBusy = false;
_broadcasterService.Unsubscribe(nameof(ViewPage));
_vm.CleanUp();
_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");
@@ -106,6 +108,7 @@ namespace Bit.App.Pages
nameof(ShowIdentityAddress),
nameof(IsDeleted),
nameof(CanEdit),
nameof(ShowUpgradePremiumTotpText)
});
}
public List<ViewPageFieldViewModel> Fields
@@ -207,21 +210,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[]
{
@@ -231,7 +235,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
{
@@ -242,12 +250,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)
{
@@ -261,19 +269,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)
{
@@ -284,9 +283,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()
@@ -414,47 +431,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())
@@ -640,7 +616,7 @@ namespace Bit.App.Pages
}
else if (id == "LoginTotp")
{
text = _totpCode;
text = TotpCodeFormatted.Replace(" ", string.Empty);
name = AppResources.VerificationCodeTotp;
}
else if (id == "LoginUri")

File diff suppressed because it is too large Load Diff

View File

@@ -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>
@@ -889,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>
@@ -2254,9 +2256,49 @@
<data name="All" xml:space="preserve">
<value>All</value>
</data>
<data name="DisplayItemsContainingTOTP" xml:space="preserve">
<value>Display items containing TOTP</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>
<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>
<data name="EnvironmentPageUrlsError" xml:space="preserve">
<value>One or more of the URLs entered are invalid. Please revise it and try to save again.</value>
</data>

View File

@@ -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}" />

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

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#52bdfb</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#52bdfb</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#175DDC</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,4 +71,6 @@
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
<Color x:Key="HyperlinkColor">#81a1c1</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

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

View 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);
}
}
}

View 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));
}
}
}